🔷 TypeScript Q92 / 101

Explain advanced type inference scenarios.

AI-Powered Answer ✓ Answered

TypeScript's type system is incredibly powerful, and its ability to infer types automatically is a cornerstone of its developer experience. While basic inference handles many common cases, advanced scenarios leverage sophisticated mechanisms like conditional types, mapped types, and the `infer` keyword to deduce complex types, enhancing type safety and developer productivity.

Conditional Types and the `infer` Keyword

Conditional types allow for type selection based on a condition, much like a ternary operator at the type level. The infer keyword is a special part of conditional types that allows you to extract a type from within another type during the inference process, making it incredibly powerful for deconstructing and transforming types.

typescript
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type Func = () => string;
type Result = MyReturnType<Func>; // Result is string

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type PromiseResult = UnwrapPromise<Promise<number>>; // PromiseResult is number
type NonPromiseResult = UnwrapPromise<string>; // NonPromiseResult is string

Mapped Types with Inference

Mapped types iterate over keys of another type to create a new type. Combined with conditional types or as clauses, they can infer new property names or values based on the original type's structure. For instance, transforming properties to be readonly or optional, or even changing their names using template literal types for property keys.

typescript
type Props = {
  name: string;
  age: number;
  isActive: boolean;
};

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<Props>;
// UserGetters is:
// {
//   getName: () => string;
//   getAge: () => number;
//   getIsActive: () => boolean;
// }

Template Literal Types

Template literal types allow TypeScript to infer string literal types based on string patterns. This is particularly useful for creating new string literal types by concatenating, transforming, or extracting parts of existing string types, enabling powerful string-based type manipulations.

typescript
type EventName = 'click' | 'hover' | 'focus';
type OnEventHandlers = `on${Capitalize<EventName>}`;
// OnEventHandlers is "onClick" | "onHover" | "onFocus"

function createHandler(eventName: OnEventHandlers, handler: () => void) {
    // ...
}

createHandler('onClick', () => {}); // Valid
// createHandler('onTap', () => {}); // Error: Argument of type '"onTap"' is not assignable to parameter of type '"onClick" | "onHover" | "onFocus"'.

Tuple Type Inference (`as const`)

TypeScript can infer tuple types more precisely, especially when using as const. This tells TypeScript to infer the narrowest possible type for a literal expression, turning mutable arrays into readonly tuples, which significantly enhances type safety when working with fixed-length sequences where element order and type are crucial.

typescript
function processTuple<T extends readonly [string, number]>(data: T) {
  const [name, value] = data;
  console.log(name.toUpperCase(), value.toFixed(2));
}

const myData = ['itemA', 123] as const;
processTuple(myData); // Valid, myData is inferred as readonly ["itemA", 123]

const anotherData: [string, number] = ['itemB', 456];
processTuple(anotherData); // Valid, array literal can be inferred as tuple if assignment target is tuple

Contextual Type Inference

Contextual typing occurs when the type of an expression is implied by its location. This is common with function arguments, object literals, and array literals, where TypeScript uses the 'target type' to infer the types of parts of the expression, reducing the need for explicit type annotations.

typescript
type User = { id: number; name: string };
const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

const userNames = users.map(user => {
  // 'user' is contextually inferred as 'User'
  return user.name.toUpperCase();
}); // userNames is inferred as string[]

const numbers: number[] = [1, 2, 3];
numbers.forEach(num => {
  // 'num' is contextually inferred as 'number'
  console.log(num.toFixed(2));
});

Function Overload Inference

When a function has multiple overload signatures, TypeScript infers the correct signature based on the arguments provided. It intelligently attempts to find the best match among the available overloads, inferring the return type and specific parameter types accordingly, which is crucial for functions designed to handle different types of inputs.

typescript
function greet(name: string): string;
function greet(count: number): number;
function greet(arg: string | number): string | number {
  if (typeof arg === 'string') {
    return `Hello, ${arg}!`;
  } else {
    return arg * 2;
  }
}

const greetingString = greet('World'); // greetingString is inferred as string
const greetingNumber = greet(5);      // greetingNumber is inferred as number

// const invalidGreeting = greet(true); // Error: No overload matches this call.