Type instantiation and lazy evaluation
Type Instantiation and Lazy Evaluation
TypeScript's type system supports type instantiation and lazy evaluation, which are particularly important when dealing with complex types. Type instantiation allows the creation of concrete types based on generic parameters, while lazy evaluation ensures that type expressions are only computed when needed, avoiding unnecessary performance overhead.
Type Instantiation
Type instantiation is the core mechanism of generics in TypeScript. When using generics, TypeScript generates concrete type instances based on the provided type parameters. This process is similar to function calls but occurs at the type level.
type Box<T> = { value: T };
// Type instantiation
type StringBox = Box<string>; // { value: string }
type NumberBox = Box<number>; // { value: number }
Type instantiation can be nested to form more complex type structures:
type Pair<T, U> = [T, U];
type BoxedPair<T, U> = Box<Pair<T, U>>;
// Multi-level instantiation
type StringNumberBox = BoxedPair<string, number>; // { value: [string, number] }
Lazy Evaluation in Conditional Types
TypeScript's conditional types support lazy evaluation, where type expressions are only computed when actually used. This feature is particularly useful when dealing with recursive or complex types.
type IsString<T> = T extends string ? true : false;
// Not immediately computed
type MaybeString = IsString<unknown>; // Remains a conditional type
// Computed only when used
type A = MaybeString extends true ? "yes" : "no"; // Evaluates to "no"
Lazy Behavior in Type Aliases
Type aliases are not immediately expanded when defined. This lazy behavior prevents infinite recursion and improves performance.
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
// Does not immediately expand the entire structure when used
type MyData = Json;
Mapped Types and Lazy Evaluation
Mapped types combined with lazy evaluation enable powerful type transformation capabilities:
type PartialNullable<T> = {
[K in keyof T]?: T[K] | null;
};
interface User {
name: string;
age: number;
}
// Computed only when used
type PartialUser = PartialNullable<User>; // { name?: string | null; age?: number | null }
Distributive Conditional Types
When conditional types are applied to union types, TypeScript distributes the condition, which is another manifestation of lazy evaluation:
type ToArray<T> = T extends any ? T[] : never;
// Distributed application
type StringOrNumberArray = ToArray<string | number>; // string[] | number[]
Lazy Evaluation in Recursive Types
Recursive type definitions rely on lazy evaluation to avoid infinite loops caused by immediate expansion:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Nested {
a: {
b: {
c: number;
};
};
}
// Recursively applied but not immediately expanded
type ReadonlyNested = DeepReadonly<Nested>;
Type Inference and Laziness
TypeScript's type inference system also leverages lazy evaluation to optimize performance:
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
// Remains lazy until used
type UnpackedArray = Unpacked<string[]>; // string
type UnpackedPromise = Unpacked<Promise<number>>; // number
Utility Type Implementations
Many built-in utility types utilize lazy evaluation:
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
function foo(a: string, b: number): boolean {
return true;
}
// Lazily infers parameters and return type
type FooParams = MyParameters<typeof foo>; // [string, number]
type FooReturn = MyReturnType<typeof foo>; // boolean
Performance Considerations in Type Operations
The lazy evaluation mechanism allows TypeScript to optimize the performance of complex types:
type ComplexType<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
"other";
// Does not precompute all branches
type Result = ComplexType<unknown>; // Remains a conditional type
// Computed only when the type is explicit
type StringResult = ComplexType<"hello">; // "string"
Edge Cases in Type Instantiation
In certain edge cases, type instantiation exhibits special behavior:
type Foo<T> = { x: T };
type Bar<U> = Foo<U>; // Aliases are not immediately instantiated
// Instantiated only when actually used
type StringBar = Bar<string>; // { x: string }
Interaction with Function Generics
Function generic parameters are instantiated only when called, another form of lazy evaluation:
function identity<T>(x: T): T {
return x;
}
// Type instantiation occurs only at call time
const result = identity("hello"); // T instantiated as string
Constraint Checking for Type Parameters
Constraint checking for type parameters also employs a lazy strategy:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// Constraints checked only at call time
loggingIdentity("text"); // OK, string has a length property
Advanced Type Patterns
Lazy evaluation enables advanced type patterns:
type Chainable<T = {}> = {
option<K extends string, V>(key: K, value: V): Chainable<T & { [P in K]: V }>;
get(): T;
};
// Incrementally builds the type
const config = {} as Chainable;
const result = config
.option("name", "TypeScript")
.option("version", 4.9)
.get(); // { name: string; version: number }
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn