TypeScript 的类型系统非常强大(重要),因为它允许用其他类型来表达类型。这个想法最简单的形式是泛型,我们实际上有各种各样的类型运算符可供使用。也可以用我们已经拥有的值来表达类型。
一. 泛型
typescript中的泛型与其它语言的类似,既可以用于函数,也可以用于类,示例:
1 function loggingIdentity<Type>(arg: Type): Type {
2 console.log(arg);
3 return arg;
4 }
5
6 interface Lengthwise {
7 length: number;
8 }
9
10 function loggingIdentityWithLengthWise<Type extends Lengthwise>(arg: Type): Type {
11 console.log(arg.length);
12 return arg;
13 }
14
15 let u1 = loggingIdentity<string>("Hello");
16 let u2 = loggingIdentityWithLengthWise<string>("Hello")
17
18 class GenericNumber<NumType> {
19 zeroValue: NumType;
20 add: ((x: NumType, y: NumType) => NumType);
21 }
22
23 let myGenericNumber = new GenericNumber<number>();
24 myGenericNumber.zeroValue = 0;
25 myGenericNumber.add = function (x, y) {
26 return x + y;
27 };
28 console.log(myGenericNumber.add(3,2));
在 TypeScript 中使用泛型创建工厂时,需要通过其构造函数引用类类型。例如,
1 class BeeKeeper {
2 hasMask: boolean = true;
3 }
4
5 class ZooKeeper {
6 nametag: string = "Mikle";
7 }
8
9 class Animal {
10 numLegs: number = 4;
11 }
12
13 class Bee extends Animal {
14 keeper: BeeKeeper = new BeeKeeper();
15 }
16
17 class Lion extends Animal {
18 keeper: ZooKeeper = new ZooKeeper();
19 }
20
21 function createInstance<A extends Animal>(c: new () => A): A {
22 return new c();
23 }
24
25 createInstance(Lion).keeper.nametag;
26 createInstance(Bee).keeper.hasMask;
二. keyof, typeof
keyof
获取类型的所有 key 的集合
1 interface Person {
2 name: string;
3 age: number;
4 }
5 type personKeys = keyof Person;
6 //等同于:type personKeys = 'name' | 'age'
7 let p1 = {
8 name: 'thia',
9 age: 30
10 }
11 function getPersonVal (k: personKeys) {
12 return p1[k]
13 }
14 /**等同于
15 function getPersonVal(k: 'name' | 'age'){
16 return p1[k]
17 }
18 */
19 getPersonVal('name') //ok
20 getPersonVal('gender') //error
typeof
在typescript中 typeof 有两种作用:
- 获取数据的类型
- 捕获数据的类型
1 let str1 = 'hello';
2 //let声明的是变量,把 'string' 作为值
3 let t = type of str1;
4 //type声明的是类型,把 ‘string’作为类型
5 type myType = typeof str1;
6
7 let a = t;
8 let str2: myType = 'world';
三. 索引访问类型
我们可以使用索引访问类型来查找另一种类型的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
type Age = number
索引类型本身就是一种类型,因此我们可以完全使用联合keyof
、 或其他类型:
type I1 = Person["age" | "name"];
type I1 = string | number
type I2 = Person[keyof Person];
type I2 = string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
type I3 = string | boolean
如果您尝试索引不存在的属性,您甚至会看到错误:
type I1 = Person["alve"];
Property 'alve' does not exist on type 'Person'.
四. 条件类型
条件类型的形式有点像condition ? trueExpression : falseExpression
JavaScript 中的条件表达式 ( ):
SomeType extends OtherType ? TrueType : FalseType;
当左侧的类型extends
可分配给右侧的类型时,您将在第一个分支(“真”分支)中获得类型;否则,您将在后一个分支(“假”分支)中获得类型。
从上面的示例中,条件类型可能不会立即有用 - 我们可以告诉自己是否Dog extends Animal
选择number
or string
!但是条件类型的强大之处在于将它们与泛型一起使用。
示例:
1 interface IdLabel {
2 id: number /* some fields */;
3 }
4 interface NameLabel {
5 name: string /* other fields */;
6 }
7
8 function createLabel(id: number): IdLabel;
9 function createLabel(name: string): NameLabel;
10 function createLabel(nameOrId: string | number): IdLabel | NameLabel;
11 function createLabel(nameOrId: string | number): IdLabel | NameLabel {
12 throw "unimplemented";
13 }
14
15 type NameOrId<T extends number | string> = T extends number
16 ? IdLabel
17 : NameLabel;
18
19 function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
20 throw "unimplemented";
21 }
22
23 let a = createLabel("typescript");
24
25 let b = createLabel(2.8);
26
27 let c = createLabel(Math.random() ? "hello" : 42);
条件类型约束
下例让typescript模板知道传入的类型有一个属性叫message
1 type MessageOf<T extends { message: unknown }> = T["message"];
2
3 interface Email {
4 message: string;
5 }
6
7 type EmailMessageContents = MessageOf<Email>;
在条件类型中推断
我们刚刚发现自己使用条件类型来应用约束,然后提取类型。这最终成为一种常见的操作,条件类型使它更容易。
条件类型为我们提供了一种方法来推断我们使用infer
关键字在真实分支中比较的类型。例如,我们可以推断元素类型,Flatten
而不是使用索引访问类型“手动”获取它:
1 type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
在这里,我们使用infer
关键字声明性地引入了一个新的泛型类型变量Item
,而不是指定如何T
在真正的分支中检索元素类型。这使我们不必考虑如何挖掘和探索我们感兴趣的类型的结构。
infer
我们可以使用关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取返回类型:
1 type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
2 ? Return
3 : never;
4
5 type Num = GetReturnType<() => number>;
6
7 type Str = GetReturnType<(x: string) => string>;
8
9 type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,会根据最后一个签名进行推断(这可能是最宽松的包罗万象的情况)。无法根据参数类型列表执行重载决议。
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>;
type T1 = string | number
五. 映射类型
当您不想重复自己时,有时一种类型需要基于另一种类型。映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型:
例:OptionsFlags将从类型中获取所有属性Type并将其值改为bool型
1 type OptionsFlags<Type> = {
2 [Property in keyof Type]: boolean;
3 };
4
5 type FeatureFlags = {
6 darkMode: () => void;
7 newUserProfile: () => void;
8 };
9
10 type FeatureOptions = OptionsFlags<FeatureFlags>;
11
12 // type FeatureOptions = {
13 // darkMode: boolean;
14 // newUserProfile: boolean;
15 // }
在 TypeScript 4.1 及更高版本中,您可以使用映射类型中的as
子句重新映射映射类型中的键:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
可以利用模板文字类型等功能从以前的属性名称中创建新的属性名称:
1 type Getters<Type> = {
2 [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
3 };
4
5 interface Person {
6 name: string;
7 age: number;
8 location: string;
9 }
10
11 type LazyPerson = Getters<Person>;
12 // type LazyPerson = {
13 // getName: () => string;
14 // getAge: () => number;
15 // getLocation: () => string;
16 // }
六. 模板文字类型
模板文字类型建立在字符串文字类型之上,并且能够通过联合扩展成许多字符串。
它们与JavaScript 中的模板文字字符串具有相同的语法,但用于类型位置。当与具体文字类型一起使用时,模板文字通过连接内容来生成新的字符串文字类型。
当在插值位置使用联合时,类型是可以由每个联合成员表示的每个可能的字符串文字的集合。对于模板文字中的每个插值位置,联合是交叉相乘的:
1 type EmailLocaleIDs = "welcome_email" | "email_heading";
2 type FooterLocaleIDs = "footer_title" | "footer_sendoff";
3
4 type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
5 // type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
使用模板文字进行推理
请注意,我们并没有从原始传递对象中提供的所有信息中受益。给定一个firstName
(即一个firstNameChanged
事件)的变化,我们应该期望回调将收到一个类型的参数string
。同样,更改的回调age
应该接收一个number
参数。我们天真地使用any
输入callBack
' 参数。同样,模板文字类型可以确保属性的数据类型与该属性的回调的第一个参数类型相同。
使这成为可能的关键见解是:我们可以使用具有泛型的函数,这样:
- 第一个参数中使用的文字被捕获为文字类型
- 该文字类型可以被验证为在泛型中的有效属性的联合中
- 可以使用 Indexed Access 在泛型结构中查找已验证属性的类型
- 然后可以应用此类型信息以确保回调函数的参数属于同一类型
1 type PropEventSource<Type> = {
2 on<Key extends string & keyof Type>
3 (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
4 };
5
6 declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
7
8 const person = makeWatchedObject({
9 firstName: "Saoirse",
10 lastName: "Ronan",
11 age: 26
12 });
13
14 person.on("firstNameChanged", newName => {
15 console.log(`new name is ${newName.toUpperCase()}`);
16 });
17
18 person.on("ageChanged", newAge => {
19
20 if (newAge < 0) {
21 console.warn("warning! negative age");
22 }
23 })
这里我们做成on
了一个泛型方法。
当用户使用字符串调用时"firstNameChanged'
,TypeScript 将尝试推断Key
. 为此,它将与Key
之前的内容匹配"Changed"
并推断字符串"firstName"
。一旦 TypeScript 确定了这一点,该on
方法就可以获取firstName
原始对象的类型,string
在本例中就是这样。同样,当使用 调用时"ageChanged"
,TypeScript 会找到属性的age
类型number
.
推理可以以不同的方式组合,通常是解构字符串,并以不同的方式重构它们。