一.索引类型(Index types)
索引类型让静态检查能够覆盖到类型不确定(无法穷举)的”动态“场景,例如:
function pluck(o, names) {
return names.map(n => o[n]);
}
pluck
函数能从o
中摘出来names
指定的那部分属性,存在2个类型约束:
参数
names
中只能出现o
身上有的属性返回类型取决于参数
o
身上属性值的类型
这两条约束都可以通过泛型来描述:
interface pluck {
<T, K extends keyof T>(o: T, names: K[]): T[K][]
}
let obj = { a: 1, b: '2', c: false };
// 参数检查
// 错误 Type 'string' is not assignable to type '"a" | "b" | "c"'.
pluck(obj, ['n']);
// 返回类型推断
let xs: (string | number)[] = pluck(obj, ['a', 'b']);
P.S.interface
能够描述函数类型,具体见二.函数
出现了2个新东西:
keyof
:索引类型查询操作符(index type query operator)T[K]
:索引访问操作符(indexed access operator):
索引类型查询操作符
keyof T
取类型T
上的所有public
属性名构成联合类型,例如:
// 等价于 let t: { a: number; b: string; c: boolean; }
let t: typeof obj;
// 等价于 let availableKeys: "a" | "b" | "c"
let availableKeys: keyof typeof obj;
declare class Person {
private married: boolean;
public name: string;
public age: number;
}
// 等价于 let publicKeys: "name" | "age"
let publicKeys: keyof Person;
P.S.注意,不同于typeof
面向值,keyof
是针对类型的,而不是值(因此keyof obj
不合法)
这种类型查询能力在pluck
等预先无法得知(或无法穷举)属性名的场景很有意义
索引访问操作符
与keyof
类似,另一种类型查询能力是按索引访问类型(T[K]
),相当于类型层面的属性访问操作符:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
let c: boolean = getProperty(obj, 'c');
// 等价于
let cValue: typeof obj['c'] = obj['c'];
也就是说,如果t: T
、k: K
,那么t[k]: T[K]
:
type typesof<T, K extends keyof T> = T[K];
let a: typesof<typeof obj, 'a'> = obj['a'];
let bOrC: typesof<typeof obj, 'b' | 'c'> = obj['b'];
bOrC = obj['c'];
// 错误 Type 'number' is not assignable to type 'string | boolean'.
bOrC = obj['a'];
索引类型与字符串索引签名
keyof
与T[K]
同样适用于字符串索引签名(index signature),例如:
interface NetCache {
[propName: string]: object;
}
// string | number 类型
let keyType: keyof NetCache;
// object 类型
let cached: typesof<NetCache, 'http://example.com'>;
注意到keyType
的类型是string | number
,而不是预期的string
,这是因为在JavaScript里的数值索引会被转换成字符串索引,例如:
let netCache: NetCache;
netCache[20190101] === netCache['20190101']
也就是说,key
的类型可以是字符串也可以是数值,即string | number
。如果非要剔除number
的话,可以通过内置的Extract
类型别名来完成:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
(摘自TypeScript/lib/lib.es5.d.ts)
let stringKey: Extract<keyof NetCache, string> = 'http://example.com';
当然,一般没有必要这样做,因为从类型角度来看,key: string | number
是合理的
P.S.更多相关讨论,见Keyof inferring string | number when key is only a string
二.映射类型
与索引类型类似,另一种从现有类型衍生新类型的方式是做映射:
In a mapped type, the new type transforms each property in the old type in the same way.
例如:
type Stringify<T> = {
[P in keyof T]: string
}
// 把所有属性值都toString()一遍
function toString<T>(obj: T): Stringify<T> {
return Object.keys(obj)
.reduce((a, k) =>
({ ...a, [k]: obj[k].toString() }),
Object.create(null)
);
}
let stringified = toString({ a: 1, b: 2 });
// 错误 Type 'number' is not assignable to type 'string'.
stringified = { a: 1 };
Stringify
实现了{ [propName: string]: any }
到{ [propName: string]: string }
的类型映射,但看起来不那么十分有用。实际上,更常见的用法是通过映射类型来改变key
的属性,比如把一个类型的所有属性都变成可选或只读:
type Partial<T> = {
[P in keyof T]?: T[P];
}
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
(摘自TypeScript/lib/lib.es5.d.ts)
let obj = { a: 1, b: '2' };
let constObj: Readonly<typeof obj>;
let optionalObj: Partial<typeof obj>;
// 错误 Cannot assign to 'a' because it is a read-only property.
constObj.a = 2;
// 错误 Type '{}' is missing the following properties from type '{ a: number; b: string; }': a, b
obj = {};
// 正确
optionalObj = {};
语法格式
最直观的例子:
// 找一个“类型集”
type Keys = 'a' | 'b';
// 通过类型映射得到新类型 { a: boolean, b: boolean }
type Flags = { [K in Keys]: boolean };
[K in Keys]
形式上与索引签名类似,只是融合了for...in
语法。其中:
K
:类型变量,依次绑定到每个属性上,对应每个属性名的类型Keys
:字符串字面量构成的联合类型,表示一组属性名(的类型)boolean
:映射结果类型,即每个属性值的类型
类似的,[P in keyof T]
只是找keyof T
作为(属性名)类型集,从而对现有类型做映射得到新类型
P.S.另外,Partial
与Readonly
都能够完整保留源类型信息(从输入的源类型中取属性名及值类型,仅存在修饰符上的差异,源类型与新类型之间有兼容关系),称为同态(homomorphic)转换,而Stringify
丢弃了源属性值类型,属于非同态(non-homomorphic)转换
“拆箱”推断(unwrapping inference)
对类型做映射相当于类型层面的“装箱”:
// 包装类型
type Proxy<T> = {
get(): T;
set(value: T): void;
}
// 装箱(普通类型 to 包装类型的类型映射)
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
// 装箱函数
function proxify<T>(o: T): Proxify<T> {
let result: Proxify<T>;
// ... wrap proxies ...
return result;
}
例如:
// 普通类型
interface Person {
name: string,
age: number
}
let lily: Person;
// 装箱
let proxyProps: Proxify<Person> = proxify(lily);
同样,也能“拆箱”:
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps: Person = unproxify(proxyProps);
能够自动推断出最后一行的unproxify
函数类型为:
function unproxify<Person>(t: Proxify<Person>): Person
从参数类型proxyProps: Proxify<Person>
中取出了Person
作为返回值类型,即所谓“拆箱”
三.条件类型
条件类型用来表达非均匀类型映射(non-uniform type mapping),能够根据类型兼容关系(即条件)从两个类型中选出一个:
T extends U ? X : Y
语义类似于三目运算符,若T
是U
的子类型,则为X
类型,否则就是Y
类型。另外,还有一种情况是条件的真假无法确定(无法确定T
是不是U
的子类型),此时为X | Y
类型,例如:
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// x 的类型为 string | number
let x = f(Math.random() < 0.5)
另外,如果T
或U
含有类型变量,就要等到类型变量都有对应的具体类型后才能得出条件类型的结果:
When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not a the type system has enough information to conclude that T is always assignable to U.
例如:
interface Foo {
propA: boolean;
propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
// a 的类型为 U extends Foo ? string : number
let a = f(x);
let b: string | number = a;
}
其中a
的类型为U extends Foo ? string : number
(即条件不确定的情况),因为f(x)
中x
的类型U
尚不确定,无从得知U
是不是Foo
的子类型。但条件类型无非两种可能类型,所以let b: string | number = a;
一定是合法的(无论x
是什么类型)
可分配条件类型
可分配条件类型(distributive conditional type)中被检查的类型是个裸类型参数(naked type parameter)。其特殊之处在于满足分配律:
(A | B | C) extends U ? X : Y
等价于
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
例如:
// 嵌套的条件类型类似于模式匹配
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" : "object";
// T 类型等价于联合类型 string" | "function
type T = TypeName<string | (() => void)>;
另外,在T extends U ? X : Y
中,X
中出现的T
都具有U
类型约束:
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
// T 类型等价于联合类型 BoxedValue<string> | BoxedArray<boolean>
type T = Boxed<string | boolean[]>;
上例中Boxed<T>
的True分支具有any[]
类型约束,因此能够通过索引访问(T[number]
)得到数组元素的类型
应用场景
条件类型结合映射类型能够实现具有针对性的类型映射(不同源类型能够对应不同的映射规则),例如:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
// 摘出所有函数类型的属性
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
// T 类型等价于字符串字面量类型 "updatePart"
type T = FunctionPropertyNames<Part>;
而可分配条件类型通常用来筛选联合类型:
type Diff<T, U> = T extends U ? never : T;
// T 类型等价于联合类型 "b" | "d"
type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
// 更进一步的
type NeverNullable<T> = Diff<T, null | undefined>;
function f1<T>(x: T, y: NeverNullable<T>) {
x = y;
// 错误 Type 'T' is not assignable to type 'Diff<T, null>'.
y = x;
}
条件类型中的类型推断
在条件类型的extends
子句中,可以通过infer
声明引入一个将被推断的类型变量,例如:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
上例中引入了类型变量R
表示函数返回类型,并在True分支中引用,从而提取出返回类型
P.S.特殊的,如果存在重载,就取最后一个签名(按照惯例,最后一个通常是最宽泛的)进行推断,例如:
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
// T 类型等价于联合类型 string | number
type T = ReturnType<typeof foo>;
P.S.更多示例见Type inference in conditional types
预定义的条件类型
TypeScript 还内置了一些常用的条件类型:
// 从 T 中去掉属于 U 的子类型的部分,即之前示例中的 Diff
type Exclude<T, U> = T extends U ? never : T;
// 从 T 中筛选出属于 U 的子类型的部分,之前示例中的 Filter
type Extract<T, U> = T extends U ? T : never;
// 从 T 中去掉 null 与 undefined 部分
type NonNullable<T> = T extends null | undefined ? never : T;
// 取出函数类型的返回类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 取出构造函数类型的示例类型
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
(摘自TypeScript/lib/lib.es5.d.ts)
具体示例见Predefined conditional types
四.总结
除类型组合外,另2种产生新类型的方式是类型查询与类型映射
类型查询:
- 索引类型:取现有类型的一部分产生新类型
类型映射:
- 映射类型:对现有类型做映射得到新类型
- 条件类型:允许以类型兼容关系为条件进行简单的三目运算,用来表达非均匀类型映射
非常抽象,能否先接触到再加深理解?