假设我们有一个用户管理系统,用户的信息通过以下格式存储:
1
2
3
4
5
6
7
8
9
10
interface User {
name: string;
phone: number;
address: {
country: string;
province: string;
city: string;
postalCode: number;
}
}
这时,我们想获取用户address的类型,最直接的办法是将address抽取为单独的类型。
1
2
3
4
5
6
7
8
9
10
11
12
interface Address {
country: string;
province: string;
city: string;
postalCode: number;
}
interface User {
name: string;
phone: number;
address: Address;
}
这段代码如果是我们自己业务可控的当然没问题,但如果它来源于第三方,我们没法直接修改代码怎么办呢?
这时候我们可以借助ts的索引访问类型(Indexed Access Types)来查找属性值的类型。
索引访问类型形如T[P],有点类似js对象的属性访问。需要注意的是这里的P是类型而不是值,否则会报错。
1
2
3
4
5
6
7
8
const address = "address";
// 报错
// 'address' refers to a value, but is being used as a type here. Did you mean 'typeof address'?
type Address = User[address];
// ok,'address'是User类型的property,是一个字面量类型
type Address = User['address'];
以上通过特定的字面量属性获取类型是索引访问类型的最基本用法,下面介绍一些高级用法。
联合类型作为索引访问
可以使用联合类型作为索引,获取多个属性值的联合类型。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface User {
name: string;
phone: number;
address: {
country: string;
province: string;
city: string;
postalCode: number;
}
}
type NameOrPhone = 'name' | 'phone';
type Username1 = User[NameOrPhone]; // string | number
type Username2 = User['name' | 'phone']; // string | number
结合keyof操作符,可以获取对象所有属性的联合类型。
1
2
3
4
5
6
7
8
type Username = User[keyof User];
// 等价于
type Username = string | number | {
country: string;
province: string;
city: string;
postalCode: number;
}
深度访问
还是上面User的例子,不同的系统设计,address.postalCode可能是number也有可能是string类型。如果我们想获取它的类型怎么办呢?类似js对象的属性访问,索引访问类型也能一层一层的深度访问。
1
2
3
4
5
6
7
8
9
10
11
12
interface User {
name: string;
phone: number;
address: {
country: string;
province: string;
city: string;
postalCode: number;
}
}
type Address = User['address']['postalCode']; // number
结合后面介绍的T[number],可以获取数组子项里的属性类型。
1
2
type Users = User[];
type Address = Users[number]['address']['postalCode']; // number
T[‘length’]
对于type StringArr = string[]这样一个数组类型,可以理解为以下的对象类型:
1
2
3
4
type StringArr = {
[index: number]: string;
length: number;
}
对于type Tuple = [string, number]这样一个元组类型,可以理解为以下的对象类型:
1
2
3
4
5
type TupleObj = {
length: 2;
0: string;
1: number;
}
通过T['length']可以获取数组或者元组的长度的类型,但需要注意的是,数组得到的是number类型,元组得到的是一个数字的字面量类型。
1
2
3
4
5
type StringArr = string[];
type Tuple = [string, number, 'foo'];
type ArrLen = StringArr['length']; // number
type TupleLen = Tuple['length']; // 3
元组T['length']得到数字字面量类型这个特性很有用,ts类型体操里凡是涉及数值计算都需要用到它,比如Length of String。
另外,由于数组和元组T['length']得出的类型不同,我们可以根据这个特性区分类型是数组还是元组。
1
2
3
4
5
6
7
type StringArr = string[];
type Tuple = [string, number, 'foo'];
type isTuple<T extends any[]> = number extends T['length'] ? false : true;
type T1 = isTuple<StringArr>;
type T2 = isTuple<Tuple>;
但是,对于有rest元素的元组,这个判断是不准确的。这个问题我也在github上发起了相关讨论。
1
2
3
4
5
6
7
8
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
// 实际上StringNumberBooleans等都是元组,但判断有误
type T1 = isTuple<StringNumberBooleans>; // false
type T2 = isTuple<StringNumberBooleans>; // false
type T3 = isTuple<StringNumberBooleans>; // false
T[string]和T[number]
索引访问不一定要使用明确的字面量类型,它还允许使用T[string]或T[number]这样的特殊形式。
对于对象类型来说,如果定义了index签名,可以通过T[string]获取属性值的所有类型的联合类型。
1
2
3
4
5
6
7
interface User {
[index: string]: string | number | boolean;
name: string;
phone: number;
}
type UserValueType = User[string]; // string | number | boolean
但这种写法不是很常用。
比较常见的是T[number]在数组和元组上的应用。
数组可以通过T[number]获取子项的类型,但要注意区分以下几种场景的细微差异:
1
2
3
4
5
6
7
8
9
type EmptyArr = []; // 实际是元组
type StringArr = string[];
type AnyArr = any[];
type StringOrNumberArr = (string | number)[];
type T1 = EmptyArr[number]; // never
type T2 = StringArr[number]; // string
type T3 = AnyArr[number]; // any
type T4 = StringOrNumberArr[number]; // string | number
type EmptyArr = []实际上是元组。
通过T[number]可以将元组转成联合类型。
1
2
3
4
5
6
7
8
9
10
type Tuple = [string, number];
type TupleObj = {
length: 2;
0: string;
1: number;
}
type T1 = Tuple[number]; // string | number
type T2 = TupleObj[0 | 1]; // string | number
这项特性在一些场景下十分有用,比如ts类型体操中的Tuple to Union以及下面介绍的与typeof操作符结合使用的真实场景。
另外值得一提的是,数组和元组都可以通过T[0]这种形式获取某一项元素的类型。
1
2
3
4
5
6
7
8
9
type EmptyArr = [];
type StringArr = string[];
type Tuple = [string, number, 'foo'];
type T1 = EmptyArr[0]; // Tuple type '[]' of length '0' has no element at index '0'.
type T2 = StringArr[0]; // string
type T3 = Tuple[0]; // string
type T4 = Tuple[1]; // number
type T5 = Tuple[2]; // "foo"
T[number] + typeof
假设有下面一段代码:
1
2
3
4
5
6
7
const platforms = ['pc', 'ios', 'android'];
function platformAdapter(p: string) {
console.log(p);
}
platformAdapter('unknow-platform');
我们在做一个页面,需要适配PC端、ios、android这3个平台,所以定义了一个叫platformAdapter的适配器方法。我们希望限制platformAdapter的入参p为指定的3个平台之一,换句话说,p的类型为'pc' | 'ios' | 'andriod',该怎么办呢?
一种办法是定义一个Platform的联合类型作为约束:
1
2
3
4
5
6
7
8
9
10
const platforms = ['pc', 'ios', 'android'];
type Platform = 'pc' | 'ios' | 'andriod';
function platformAdapter(p: Platform) {
console.log(p);
}
// Argument of type '"unknow-platform"' is not assignable to parameter of type 'Platform'.
platformAdapter('unknow-platform');
但这样在代码书写上不简洁,因为const platforms和type Platform用了相同的一些字段。而且,如果以后要新增平台,比如华为的HarmonyOS,需要同时修改两处代码,容易出错。
更好的办法是结合T[number] + typeof操作符 + as const从js常量中推断出联合类型。
1
2
3
4
5
6
7
8
9
10
const platforms = ['pc', 'ios', 'android'] as const;
type Platform = typeof platforms[number];
function platformAdapter(p: Platform) {
console.log(p);
}
// Argument of type '"unknow-platform"' is not assignable to parameter of type '"pc" | "ios" | "android"'.
platformAdapter('unknow-platform');
这里推断的过程为:
- const断言保证
platforms推断出的是readonly的字面量元组; - typeof操作符获取类型;
T[number]将元组转成联合类型'pc' | 'ios' | 'andriod'。
总结
- 索引访问类型可以帮组我们查找属性的类型;
- 可以使用联合类型作为索引访问;
- 可以多层深度访问;
- 数组和元组
T['length']得到的类型的不同,这在类型体操里非常有用; T[number]可以将元组转成联合类型,结合typeof操作符使用,在一些业务场景下很有用。
参考资料
- https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html
- https://www.allthingstypescript.dev/p/indexed-access-types-in-typescript
- https://yayujs.com/handbook/IndexedAccessTypes.html#%E7%B4%A2%E5%BC%95%E8%AE%BF%E9%97%AE%E7%B1%BB%E5%9E%8B-indexed-access-types
- https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types
