Index signatures and mapped types
TypeScript's index signatures and mapped types are core tools for handling dynamic properties and batch type transformations. Index signatures allow objects to have flexible structures, while mapped types can generate new types based on existing ones. Combining these two features can significantly enhance code flexibility and maintainability.
Basic Syntax of Index Signatures
Index signatures allow us to define the types of unknown properties in an object. The syntax is [key: KeyType]: ValueType
, where KeyType
is typically string
, number
, or symbol
. For example:
interface DynamicObject {
[key: string]: number;
}
const scores: DynamicObject = {
math: 90,
physics: 85,
// Allows dynamic property addition
chemistry: 88
};
When index signatures coexist with known properties, the types of the known properties must be subsets of the index signature type:
interface MixedObject {
name: string; // Error: string is incompatible with number
[key: string]: number;
}
// Correct implementation
interface ValidMixedObject {
name: never; // Use never to prohibit this property
[key: string]: number;
}
Practical Applications of Index Signatures
Index signatures are particularly useful for handling dynamic data structures. For example, processing dictionary data returned by an API:
interface ApiResponse {
status: number;
data: {
[resourceType: string]: any;
};
}
const response: ApiResponse = {
status: 200,
data: {
users: [{ id: 1, name: "Alice" }],
products: [{ id: 101, price: 99 }]
}
};
Numeric index signatures are commonly used for array-like objects:
interface StringArray {
[index: number]: string;
length: number;
}
const arr: StringArray = ["a", "b"];
console.log(arr[0]); // "a"
Core Concepts of Mapped Types
Mapped types use the in
keyword to iterate over union types and generate new types. The basic syntax is:
type Keys = "name" | "age";
type Person = {
[K in Keys]: string;
};
// Equivalent to
type Person = {
name: string;
age: string;
};
TypeScript provides built-in mapped types like Partial<T>
and Readonly<T>
:
interface Todo {
title: string;
completed: boolean;
}
type PartialTodo = Partial<Todo>;
// { title?: string; completed?: boolean }
type ReadonlyTodo = Readonly<Todo>;
// { readonly title: string; readonly completed: boolean }
Advanced Mapped Type Techniques
Combine with conditional types for more complex transformations:
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }
Remap key names using the as
clause:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
Combining Index Signatures and Mapped Types
Mapped types can modify the behavior of index signatures:
type MakeOptional<T> = {
[K in keyof T]?: T[K];
} & {
[key: string]: unknown;
};
interface Config {
apiUrl: string;
timeout: number;
}
type FlexibleConfig = MakeOptional<Config>;
// Optional known properties while allowing unknown properties
Particularly useful when working with enum types:
enum LogLevel {
ERROR,
WARN,
INFO
}
type LogConfig = {
[Level in LogLevel]: string;
};
// Equivalent to
type LogConfig = {
0: string;
1: string;
2: string;
};
Performance Considerations and Best Practices
Overusing index signatures can weaken type safety. Recommended practices include:
- Prefer precise types where possible.
- Add as specific type constraints as possible to index signatures.
- Consider using the
Record
utility type for large objects.
// Better than index signatures
type PageInfo = Record<"home" | "about" | "contact", string>;
// Explicitly restrict value types
interface SafeDictionary {
[key: string]: "read" | "write" | "execute";
}
Practical Type Gymnastics Examples
Implement a deep readonly type:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Nested {
a: number;
b: {
c: boolean;
d: {
e: string;
};
};
}
type ReadonlyNested = DeepReadonly<Nested>;
Handling union type key-value pairs:
type Flip<T extends Record<string, any>> = {
[K in keyof T as `${T[K]}`]: K;
};
type Original = { a: "1"; b: "2" };
type Flipped = Flip<Original>; // { "1": "a"; "2": "b" }
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:类型别名与接口比较
下一篇:条件类型与分布式条件类型