一、条件类型
在代码中,三元表达式是常见的一种语法,如:
let a = 10
let b = a === 10 ? a : 5
在typescript的类型系统中,同样可以使用三元表达式,先看一个简单示例:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // number
type Example2 = RegExp extends Animal ? number : string; // string
简单来说,在类型系统中,可以根据一个类型是否满足一个类型约束,从而确定具体的类型。 语法如下:SomeType extends OtherType ? TrueType : FalseType;这个类型三元表达式乍一看好像没什么用,还不如直接进行类型定义来的简单明了,但确实在某些场景下,是极其方便的,如下:
-
语法简化
假定存在这么一个场景,有一个方法,接收一个参数,根据参数的类型不同,返回不同的类型。具体定义如下:
interface IdLabel { id: number } interface NameLabel { name: string } function createLabel(id: number): IdLabel; function createLabel(name: string): NameLabel; function createLabel(nameOrId: string | number): IdLabel | NameLabel { throw "unimplemented"; } let a = createLabel(1) // IdLable let b = createLabel('a') // NameLabel在上述中,采用了重载来解决类型推导的问题。但当我们不断的新增类型时,这个重载会越来越多,这会导致阅读和维护上的问题。因此采用条件类型,可以很简洁的解决这个问题。如下:
interface IdLabel { id: number /* some fields */; } interface NameLabel { name: string /* other fields */; } // 条件类型 type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel; function createLabel<T extends number | string>(idOrName: T): NameOrId<T> { throw "unimplemented"; } let a = createLabel(1) // IdLable let b = createLabel('a') // NameLabel就是利用泛型的来进行参数类型的约束,根据具体的类型,使用条件类型来确定其返回类型。这样一个条件类型的声明,可以直接替换掉重载的定义。
-
类型推断
除了上述的语法简化上的好处,更方便的一个特性在于其使用infer关键词,可以实现自动的类型推导。官方提供了一个类型ReturnType便是基于此实现,下面简单进行分析。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;-
泛型T进行类型约束
泛型T约束了类型变量必须是一个方法类型。
-
泛型R进行类型推导
type A = () => boolean type B = ReturnType<A>我以上述示例进行举例。当传入一个方法类型A时,会进行判断是否满足约束。但在判断中使用infer声明了泛型变量R,此时则无法进行判断,因为R是一个变量,这个约束并不固定,此时会由判断逻辑变更为匹配逻辑,匹配满足的约束。很显然,此时类型A返回类型是boolean,则变量R也必须是boolean才满足,则此时会自动推导出类型boolean,如果没有匹配的约束,则返回false分支的类型any。
需要注意的是,类型推断使用infer声明的泛型变量必须与ture分支的变量一致。有点类似解一个一元方程:
//y = a + x //y:传入的类型 () => boolean //a:参数的约束 () => (...args:any) y的参数没有,是满足约束的 //x:返回的约束 () => infer R : boolean -
-
联合类型
需要注意,当使用泛型时,如果传入的类型参数是一个联合类型,则新类型一般也是一个联合类型。如下:
type ToArray<Type> = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray<string | number> // string[] | number[]在上述中,如果想得到(string | number)[],则需要特殊写法:
type ToArray<Type> = [Type] extends [any] ? Type[] : never; type StrArrOrNumArr = ToArray<string | number> // (string | number)[]
二、映射类型
当想根据一个已有类型快速创建另一种类型时,可能会采用的类型映射。简单来说,就是利用in和keyof操作符,快速的将一个已有类型的属性名获取出来。常规用法如下:
type OptionsFlags<Type> = {
[Property in keyof Type]: Type[Property];
};
-
keyof
keyof操作符会根据Type类型的属性名生成一个属性名的联合类型
-
in
遍历keyof操作符生成的联合类型的每一个类型,作为新类型的属性名
1.映射修饰符
当没有任何修饰符的时候,新类型将保持原有类型的修饰符。如下:
type A = {
a:number,
b?:boolean,
readonly c:string
}
type B = {
[prop in keyof A]:boolean
}
// type B = {
// a: boolean;
// b?: boolean | undefined;
// readonly c: boolean;
// }
-
readonly修饰符
当想为新类型增减readonly修饰符时,可以采用如下操作:
type A = { a:number, b?:boolean, readonly c:string } type B = { readonly [prop in keyof A]:boolean } // type B = { // readonly a: boolean; // readonly b?: boolean | undefined; // readonly c: boolean; // } // ================================= type A = { a:number, b?:boolean, readonly c:string } type B = { -readonly [prop in keyof A]:boolean } // type B = { // a: boolean; // b?: boolean | undefined; // c: boolean; // } -
?修饰符
当想为新类型增减?修饰符时,可以采用如下操作:
type A = { a:number, b?:boolean, readonly c:string } type B = { [prop in keyof A]?:boolean } // type B = { // a?: boolean | undefined; // b?: boolean | undefined; // readonly c?: boolean | undefined; // } // ===================================== type A = { a:number, b?:boolean, readonly c:string } type B = { [prop in keyof A]-?:boolean } // type B = { // a: boolean; // b: boolean; // readonly c: boolean; // }
2.重命名
映射操作的本质是一个遍历,因此可以在遍历途中进行属性重命名。简单语法如下:
type A = {
a:string,
b:number,
c:boolean
}
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
// 遍历每一个属性名时,添加get前缀,并将属性名称大写
};
type B = Getters<A>
// type B = {
// getA: () => string;
// getB: () => number;
// getC: () => boolean;
// }
在上述操作中,主要用到了一个叫做模板类型操作的技术。
-
移除属性
可能在某些时候,我们不想要某些属性,此时只需要将这些属性的类型变为never即可。
type RemoveKindField<Type> = { [Property in keyof Type as Exclude<Property, "kind">]: Type[Property] }; interface Circle { kind: string; radius: number; } type KindlessCircle = RemoveKindField<Circle>; // type KindlessCircle = { // radius: number; // }在类型映射操作时,当Property是kind时,Exclude<Property, "kind">是never,因此kind在新类型中被移除。当然,这种写法不灵活,但只要满足as的对象是一个类型,则语法上都是满足,如下示例:
type RemoveKindField<Type,E extends keyof Type> = { [Property in keyof Type as `get${Capitalize<string & (Property extends E ? never : Property)>}` ]:() => Type[Property] }; interface Circle { kind: string; radius: number; long:number } type KindlessCircle = RemoveKindField<Circle,"radius">; // type KindlessCircle = { // getKind: () => string; // getLong: () => number; // }上述映射操作部分的语法是很复杂的,但依旧可以正常推断类型,并且这种写法变得十分灵活,根据泛型参数的传入,可以随意移除属性。
三、模板字面量类型
模板字面量类型其实很类似javascript的模板语法,只不过是在typescript的类型系统中使用。简单例子如下:
type World = "world";
type Greeting = `hello ${World}`;
将已有类型放入模板块中,生成一个新的类型。需要注意的是,当模板中是一个联合类型时,那么新类型是所有的可能类型。
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
除此之外,还有更复杂的场景,同时存在几个类型变量
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // 4
type Lang = "en" | "ja" | "pt"; // 3
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`; // 3 * 4 = 12
此时LocaleMessageIDs的类型是12种类型组合而成的联合类型
不仅如此,官方也预定义了几种类型,是编译器自带,无法在d.ts中找到具体定义的。
- Uppercase 全部大写
- Lowercase 全部小写
- Capitalize 首字母大写
- Uncapitalize 首字母小写
type A = Uppercase<"hello"> // HELLO
type B = Lowercase<"HELLO"> // hello
type C = Capitalize<'hello'> // Hello
type D = Uncapitalize<'HELLO'> // hELLO
需要注意的是,示例中的hello字符串是一个字面量类型,不是值。