Generic constraints limit what types can be used with a generic type using the 'extends' keyword. Example: function getLength<T extends { length: number }>(arg: T): number { return arg.length; }. This ensures that the generic type T must have a length property. Constraints help enforce type safety while maintaining flexibility.
The infer keyword extracts type information within conditional types. Example: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any. This extracts the return type R from a function type. infer is powerful for type inference and extraction in complex scenarios.
Generic type defaults provide fallback types when no type argument is specified. Example: interface Container<T = string> { value: T; }. When no type is provided, string is used: let container: Container = { value: 'hello' }. They help make generic types more convenient to use.
Generic type guards combine generics with type predicates. Example: function isOfType<T>(value: any, property: keyof T): value is T { return property in value; }. They provide type-safe runtime checks while maintaining generic type information.
Generic constraints can use union and intersection types to create complex type requirements. Example: function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; }. This ensures type parameters meet specific criteria while allowing flexible combinations.
Generic type assertions allow specifying type parameters when using type assertions. Example: function create<T>(factory: { new(): T }): T { return new factory() as T; }. They provide type safety when working with dynamic type creation.
Generic event emitters provide type safety for event handling. Example: class EventEmitter<Events extends Record<string, any>> { emit<E extends keyof Events>(event: E, data: Events[E]): void { ... } }. This ensures event names and data types match at compile time.
Generic classes use type parameters to create flexible, reusable class definitions. Example: class Stack<T> { private items: T[] = []; push(item: T) { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } }. They maintain type safety while working with different data types.
Branded types are nominal types created using intersection with unique symbols. Example: type Brand<T, B> = T & { __brand: B }; type USD = Brand<number, 'USD'>. They provide type safety by preventing mixing of similarly structured but semantically different types.
Variadic tuple types allow working with tuples of variable length in a type-safe way. Example: type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]. They're useful for type-safe array operations and function parameter handling.
Template literal types can be combined with generics to create dynamic string types. Example: type PropEventType<T extends string> = `${T}Changed`; This creates new string literal types based on generic parameters.
Generic default types can be combined with constraints to provide flexible yet safe defaults. Example: interface Container<T extends object = { id: string }> { data: T; }. This ensures the default type meets the constraint while allowing other compatible types.
Generic interfaces are interfaces that can work with multiple types. Example: interface Container<T> { value: T; getValue(): T; }. Implementation: class NumberContainer implements Container<number> { constructor(public value: number) {} getValue(): number { return this.value; } }. They enable type-safe, reusable interface definitions.
TypeScript can infer generic types from usage context. Example: function identity<T>(arg: T): T { return arg; } let output = identity('hello'); // T is inferred as string. Type inference reduces verbosity while maintaining type safety. The compiler uses argument types to determine generic type parameters.
Union and intersection types can be used with generics to create flexible type combinations. Example: function process<T, U>(value: T | U): T & U { ... }. This allows working with types that can be either T or U while producing a type that has properties of both.
The keyof operator can be used with generics to create type-safe property access. Example: function getNestedValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }. This ensures type safety when accessing object properties dynamically.
Generics are a way to create reusable components that can work with multiple types while maintaining type safety. They allow you to write functions, classes, and interfaces that can work with any data type while preserving type information. Example: function identity<T>(arg: T): T { return arg; }. Generics provide type safety, code reusability, and prevent code duplication.
Conditional types select a type based on a condition, using syntax similar to ternary operators: T extends U ? X : Y. Example: type NonNullable<T> = T extends null | undefined ? never : T. They're powerful for creating complex type transformations and can be used with mapped types and unions.
Type parameters are placeholders for types in generic definitions, conventionally denoted by T, U, K, V, etc. Example: function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; }. They can have constraints, defaults, and be used in multiple places within the generic definition.
Mapped types create new types by transforming properties of existing types. Example: type Readonly<T> = { readonly [P in keyof T]: T[P] }. They can add/remove modifiers (readonly, optional) and transform property types. Mapped types are powerful for type transformations and creating utility types.
Multiple type parameters allow generics to work with multiple types simultaneously. Example: function pair<T, U>(first: T, second: U): [T, U] { return [first, second]; }. The order and naming of type parameters matter for readability and understanding the relationship between types.
Index types allow type-safe access to properties using dynamic keys. Example: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }. This ensures type safety when accessing object properties dynamically.
Generic type aliases create reusable type definitions with type parameters. Example: type Result<T> = { success: boolean; data?: T; error?: string; }. They provide flexibility while maintaining type safety and can be used with unions, intersections, and mapped types.
Distributive conditional types apply conditions to each member of a union type. Example: type ToArray<T> = T extends any ? T[] : never; type NumberOrStringArray = ToArray<number | string>; // number[] | string[]. They're useful for transforming union types.