Basic concepts of mapping types
Basics of Mapped Types
TypeScript's mapped types are a powerful tool for creating new types based on existing ones. They allow us to generate new types by iterating over the properties of an existing type and applying transformation rules, which is particularly useful when working with complex types.
Basic Syntax
The core syntax of mapped types uses the in
keyword to iterate over a union type:
type MappedType<T> = {
[P in keyof T]: T[P];
};
This simplest mapped type essentially creates a new type identical to the original type T
. keyof T
retrieves a union type of all property names of type T
, and P in
iterates over each property in this union type.
Common Built-in Mapped Types
TypeScript includes several commonly used built-in mapped types:
// Makes all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Makes all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Makes all properties read-only
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Property Modifier Operations
Mapped types can manipulate property modifiers (?
and readonly
):
// Removes the optional modifier
type Concrete<T> = {
[P in keyof T]-?: T[P];
};
// Adds the readonly modifier
type Locked<T> = {
+readonly [P in keyof T]: T[P];
};
// Removes the readonly modifier
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
Key Remapping
TypeScript 4.1 introduced key remapping, allowing property names to be modified during the mapping process:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Equivalent to:
// {
// getName: () => string;
// getAge: () => number;
// }
Combining Conditional Types with Mapped Types
Mapped types can be combined with conditional types to achieve more complex type transformations:
type NullableProperties<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type NullableUser = NullableProperties<User>;
// Equivalent to:
// {
// id: number | null;
// name: string | null;
// email: string | null;
// }
Filtering Properties
Conditional types can be used to filter out certain properties:
type OnlyFunctions<T> = {
[P in keyof T as T[P] extends Function ? P : never]: T[P];
};
interface Mixed {
id: number;
name: string;
update: (data: Partial<Mixed>) => void;
delete: () => Promise<void>;
}
type Functions = OnlyFunctions<Mixed>;
// Equivalent to:
// {
// update: (data: Partial<Mixed>) => void;
// delete: () => Promise<void>;
// }
Practical Application Examples
Form Type Generation
interface User {
username: string;
password: string;
age: number;
}
type FormField<T> = {
value: T;
isValid: boolean;
errorMessage?: string;
};
type UserForm = {
[P in keyof User]: FormField<User[P]>;
};
// Equivalent to:
// {
// username: FormField<string>;
// password: FormField<string>;
// age: FormField<number>;
// }
API Response Wrapping
type ApiResponse<T> = {
data: T;
status: number;
timestamp: Date;
};
type Entity = {
id: string;
createdAt: Date;
};
type EntityResponse<T extends Entity> = ApiResponse<{
[P in keyof T]: T[P];
}>;
interface Product extends Entity {
name: string;
price: number;
}
type ProductResponse = EntityResponse<Product>;
// Equivalent to:
// ApiResponse<{
// id: string;
// createdAt: Date;
// name: string;
// price: number;
// }>
Advanced Patterns
Recursive Mapping
Mapped types can be recursively applied to nested objects:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
interface Company {
name: string;
departments: {
name: string;
employees: number;
}[];
}
type ReadonlyCompany = DeepReadonly<Company>;
// Equivalent to:
// {
// readonly name: string;
// readonly departments: readonly {
// readonly name: string;
// readonly employees: number;
// }[];
// }
Union Type Mapping
Mapped types can also be used with union types:
type EventMap = {
click: MouseEvent;
focus: FocusEvent;
keydown: KeyboardEvent;
};
type Handlers<T> = {
[P in keyof T as `on${Capitalize<string & P>}`]: (event: T[P]) => void;
};
type EventHandlers = Handlers<EventMap>;
// Equivalent to:
// {
// onClick: (event: MouseEvent) => void;
// onFocus: (event: FocusEvent) => void;
// onKeydown: (event: KeyboardEvent) => void;
// }
Performance Considerations
While mapped types are very powerful, performance issues should be considered when dealing with large or deeply nested types. The TypeScript compiler needs to expand these types, and complex mapped types may lead to slower compilation or increased memory usage.
// Avoid overly nested mapped types
type OverlyComplex<T> = {
[P in keyof T]: {
[Q in keyof T[P]]: {
[R in keyof T[P][Q]]: T[P][Q][R];
};
};
};
Combining with Template Literal Types
Template literal types introduced in TypeScript 4.1 can be perfectly combined with mapped types:
type WithChanges<T> = {
[P in keyof T as `${string & P}Changed`]: (newValue: T[P]) => void;
};
interface Settings {
theme: string;
fontSize: number;
}
type SettingsChanges = WithChanges<Settings>;
// Equivalent to:
// {
// themeChanged: (newValue: string) => void;
// fontSizeChanged: (newValue: number) => void;
// }
Utility Type Implementations
Many utility types are implemented based on mapped types:
// Select specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Exclude specific properties
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// Record type
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Type Predicates and Mapped Types
Mapped types can be combined with type predicates to create more precise type guards:
type TypeGuardMap<T> = {
[P in keyof T]: (value: unknown) => value is T[P];
};
interface Config {
port: number;
hostname: string;
ssl: boolean;
}
const configGuards: TypeGuardMap<Config> = {
port: (value): value is number => typeof value === 'number',
hostname: (value): value is string => typeof value === 'string',
ssl: (value): value is boolean => typeof value === 'boolean',
};
Mapped Types and Generic Constraints
Mapped types can be used with generic constraints to create safer APIs:
type NonMethodKeys<T> = {
[P in keyof T]: T[P] extends Function ? never : P;
}[keyof T];
type DataProperties<T> = Pick<T, NonMethodKeys<T>>;
class User {
id: string;
name: string;
save() {
// Save logic
}
}
type UserData = DataProperties<User>;
// Equivalent to:
// {
// id: string;
// name: string;
// }
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:标记线与标记点