多态的this
类型
多态的this
类型表示的是某个包含类或接口的_子类型_。 这被称做_F_-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回this
类型:
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
let v = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();
由于这个类使用了this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
let v = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();
如果没有this
类型,ScientificCalculator
就不能够在继承BasicCalculator
的同时还保持接口的连贯性。 multiply
将会返回BasicCalculator
,它并没有sin
方法。 然而,使用this
类型,multiply
会返回this
,在这里就是ScientificCalculator
。
索引类型(Index types)
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。
function pluck(o, propertyNames) {
return propertyNames.map(n => o[n]);
}
下面是如何在TypeScript里使用此函数,通过索引类型查询和索引访问操作符:
function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
return propertyNames.map(n => o[n]);
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: 'Toyota',
model: 'Camry',
year: 2014
};
// Manufacturer and model are both of type string,
// so we can pluck them both into a typed string array
let makeAndModel: string[] = pluck(taxi, ['manufacturer', 'model']);
// If we try to pluck model and year, we get an
// array of a union type: (string | number)[]
let modelYear = pluck(taxi, ['model', 'year'])
编译器会检查manufacturer
和model
是否真的是Car
上的一个属性。 本例还引入了几个新的类型操作符。 首先是keyof T
,索引类型查询操作符。 对于任何类型T
,keyof T
的结果为T
上已知的公共属性名的联合。 例如:
let carProps: keyof Car; // the union of ('manufacturer' | 'model' | 'year')
keyof Car
是完全可以与'manufacturer' | 'model' | 'year'
互相替换的。 不同的是如果你添加了其它的属性到Car
,例如ownersAddress: string
,那么keyof Car
会自动变为'manufacturer' | 'model' | 'year' | 'ownersAddress'
。 你可以在像pluck
函数这类上下文里使用keyof
,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给pluck
:
// error, 'unknown' is not in 'manufacturer' | 'model' | 'year'
pluck(taxi, ['year', 'unknown']);
第二个操作符是T[K]
,索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着person['name']
具有类型Person['name']
— 在我们的例子里则为string
类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K]
,这正是它的强大所在。 你只要确保类型变量K extends keyof T
就可以了。 例如下面getProperty
函数的例子:
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName] is of type T[K]
}
getProperty
里的o: T
和propertyName: K
,意味着o[propertyName]: T[K]
。 当你返回T[K]
的结果,编译器会实例化键的真实类型,因此getProperty
的返回值类型会随着你需要的属性改变。
let name: string = getProperty(taxi, 'manufacturer');
let year: number = getProperty(taxi, 'year');
// error, 'unknown' is not in 'manufacturer' | 'model' | 'year'
let unknown = getProperty(taxi, 'unknown');
索引类型和字符串索引签名
keyof
和T[K]
与字符串索引签名进行交互。索引签名的参数类型必须为number
或string
。 如果你有一个带有字符串索引签名的类型,那么keyof T
会是string | number
。 (并非只有string
,因为在JavaScript里,你可以使用字符串object['42']
或 数字object[42]
索引来访问对象属性)。 并且T[string]
为索引签名的类型:
interface Dictionary<T> {
[key: string]: T;
}
let keys: keyof Dictionary<number>; // string | number
let value: Dictionary<number>['foo']; // number
如果一个类型带有数字索引签名,那么keyof T
为number
。
interface Dictionary<T> {
[key: number]: T;
}
let keys: keyof Dictionary<number>; // number
let value: Dictionary<number>['foo']; // Error, Property 'foo' does not exist on type 'Dictionary<number>'.
let value: Dictionary<number>[42]; // number
映射类型
一个常见的任务是将一个已知的类型每个属性都变为可选的:
interface PersonPartial {
name?: string;
age?: number;
}
或者我们想要一个只读版本:
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为readonly
类型或可选的。 下面是一些例子:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
像下面这样使用:
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
需要注意的是这个语法描述的是类型而非成员。 若想添加成员,则可以使用交叉类型:
// 这样使用
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
} & { newMember: boolean }
// 不要这样使用
// 这会报错!
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
newMember: boolean;
}
下面来看看最简单的映射类型和它的组成部分:
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
它的语法与索引签名的语法类型,内部使用了for .. in
。 具有三个部分:
- 类型变量
K
,它会依次绑定到每个属性。 - 字符串字面量联合的
Keys
,它包含了要迭代的属性名的集合。 - 属性的结果类型。
在个简单的例子里,Keys
是硬编码的属性名列表并且属性类型永远是boolean
,因此这个映射类型等同于:
type Flags = {
option1: boolean;
option2: boolean;
}
在真正的应用里,可能不同于上面的Readonly
或Partial
。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是keyof
和索引访问类型要做的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
但它更有用的地方是可以有一些通用版本。
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }
在这些例子里,属性列表是keyof T
且结果类型是T[P]
的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是同态的,映射只作用于T
的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设Person.name
是只读的,那么Partial<Person>.name
也将是只读的且为可选的。
下面是另一个例子,T[P]
被包装在Proxy<T>
类里:
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
// ... wrap proxies ...
}
let proxyProps = proxify(props);
注意Readonly<T>
和Partial<T>
用处不小,因此它们与Pick
和Record
一同被包含进了TypeScript的标准库里:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Record<K extends keyof any, T> = {
[P in K]: T;
}
Readonly
,Partial
和Pick
是同态的,但Record
不是。 因为Record
并不需要输入类型来拷贝属性,所以它不属于同态:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
由映射类型进行推断
现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:
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 = unproxify(proxyProps);
注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
有条件类型
TypeScript 2.8引入了_有条件类型_,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
上面的类型意思是,若T
能够赋值给U
,那么类型是X
,否则为Y
。
有条件的类型T extends U ? X : Y
或者_解析_为X
,或者_解析_为Y
,再或者_延迟_解析,因为它可能依赖一个或多个类型变量。 若T
或U
包含类型参数,那么是否解析为X
或Y
或推迟,取决于类型系统是否有足够的信息来确定T
总是可以赋值给U
。
下面是一些类型可以被立即解析的例子:
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// Type is 'string | number
let x = f(Math.random() < 0.5)
另外一个例子涉及TypeName
类型别名,它使用了嵌套了有条件类型:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"
下面是一个有条件类型被推迟解析的例子:
interface Foo {
propA: boolean;
propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
// Has type 'U extends Foo ? string : number'
let a = f(x);
// This assignment is allowed though!
let b: string | number = a;
}
这里,a
变量含有未确定的有条件类型。 当有另一段代码调用foo
,它会用其它类型替换U
,TypeScript将重新计算有条件类型,决定它是否可以选择一个分支。
与此同时,我们可以将有条件类型赋值给其它类型,只要有条件类型的每个分支都可以赋值给目标类型。 因此在我们的例子里,我们可以将U extends Foo ? string : number
赋值给string | number
,因为不管这个有条件类型最终结果是什么,它只能是string
或number
。
分布式有条件类型
如果有条件类型里待检查的类型是naked type parameter
,那么它也被称为“分布式有条件类型”。 分布式有条件类型在实例化时会自动分发成联合类型。 例如,实例化T extends U ? X : Y
,T
的类型为A | B | C
,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
。
例子
type T10 = TypeName<string | (() => void)>; // "string" | "function"
type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>; // "object"
在T extends U ? X : Y
的实例化里,对T
的引用被解析为联合类型的一部分(比如,T
指向某一单个部分,在有条件类型分布到联合类型之后)。 此外,在X
内对T
的引用有一个附加的类型参数约束U
(例如,T
被当成在X
内可赋值给U
)。
例子
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
type T20 = Boxed<string>; // BoxedValue<string>;
type T21 = Boxed<number[]>; // BoxedArray<number>;
type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
注意在Boxed<T>
的true
分支里,T
有个额外的约束any[]
,因此它适用于T[number]
数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。
有条件类型的分布式的属性可以方便地用来_过滤_联合类型:
type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>; // string | number
type T33 = Filter<string | number | (() => void), Function>; // () => void
type NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from T
type T34 = NonNullable<string | number | undefined>; // string | number
type T35 = NonNullable<string | string[] | null | undefined>; // string | string[]
function f1<T>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
}
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
let s1: string = x; // Error
let s2: string = y; // Ok
}
有条件类型与映射类型结合时特别有用:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T40 = FunctionPropertyNames<Part>; // "updatePart"
type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。
例子
// 在 TypeScript 4.1 之前的版本会报错。
// TypeScript 4.1 改进了对递归的有条件类型的支持,详情参考 4.1 版本发布说明
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;
有条件类型中的类型推断
现在在有条件类型的extends
子语句中,允许出现infer
声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer
。
例如,下面代码会提取函数类型的返回值类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number
相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
当推断具有多个调用签名(例如函数重载类型)的类型时,用_最后_的签名(大概是最自由的包含所有情况的签名)进行推断。 无法根据参数类型列表来解析重载。
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>; // string | number
无法在正常类型参数的约束子语句中使用infer
声明:
type ReturnType<T extends (...args: any[]) => infer R> = R; // 错误,不支持
但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:
type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
预定义的有条件类型
TypeScript 2.8在lib.d.ts
里增加了一些预定义的有条件类型:
Exclude<T, U>
— 从T
中剔除可以赋值给U
的类型。Extract<T, U>
— 提取T
中可以赋值给U
的类型。NonNullable<T>
— 从T
中剔除null
和undefined
。ReturnType<T>
— 获取函数返回值类型。InstanceType<T>
— 获取构造函数类型的实例类型。
Example
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Exclude<string | number | (() => void), Function>; // string | number
type T03 = Extract<string | number | (() => void), Function>; // () => void
type T04 = NonNullable<string | number | undefined>; // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
function f1(s: string) {
return { a: 1, b: s };
}
class C {
x = 0;
y = 0;
}
type T10 = ReturnType<() => string>; // string
type T11 = ReturnType<(s: string) => void>; // void
type T12 = ReturnType<(<T>() => T)>; // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
type T14 = ReturnType<typeof f1>; // { a: number, b: string }
type T15 = ReturnType<any>; // any
type T16 = ReturnType<never>; // never
type T17 = ReturnType<string>; // Error
type T18 = ReturnType<Function>; // Error
type T20 = InstanceType<typeof C>; // C
type T21 = InstanceType<any>; // any
type T22 = InstanceType<never>; // never
type T23 = InstanceType<string>; // Error
type T24 = InstanceType<Function>; // Error
注意:
Exclude
类型是建议的Diff
类型的一种实现。我们使用Exclude
这个名字是为了避免破坏已经定义了Diff
的代码,并且我们感觉这个名字能更好地表达类型的语义。