Typescript 中的类型操作(Type operations in typescript)

搬运自官网:https://www.typescriptlang.org/docs/handbook/2/types-from-types.html

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';

三. 索引访问类型

我们可以使用索引访问类型来查找另一种类型的特定属性:

索引类型本身就是一种类型,因此我们可以完全使用联合、 或其他类型:

keyof

如果您尝试索引不存在的属性,您甚至会看到错误:

Property 'alve' does not exist on type 'Person'.

四. 条件类型

条件类型的形式有点像JavaScript 中的条件表达式 ( ):

condition ? trueExpression : falseExpression

SomeType extends OtherType ? TrueType : FalseType;

当左侧的类型可分配给右侧的类型时,您将在第一个分支(“真”分支)中获得类型;否则,您将在后一个分支(“假”分支)中获得类型。

extends

从上面的示例中,条件类型可能不会立即有用 – 我们可以告诉自己是否选择or !但是条件类型的强大之处在于将它们与泛型一起使用。

Dog extends Animal
number
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[]>;

当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,会根据最后一个签名进行推断(这可能是最宽松的包罗万象的情况)。无法根据参数类型列表执行重载决议。

五. 映射类型

当您不想重复自己时,有时一种类型需要基于另一种类型。映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型:

例: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
 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

当用户使用字符串调用时,TypeScript 将尝试推断. 为此,它将与之前的内容匹配并推断字符串。一旦 TypeScript 确定了这一点,该方法就可以获取原始对象的类型,在本例中就是这样。同样,当使用 调用时,TypeScript 会找到属性的类型.

"firstNameChanged'
Key
Key
"Changed"
"firstName"
on
firstName
string
"ageChanged"
age
number

推理可以以不同的方式组合,通常是解构字符串,并以不同的方式重构它们。

————————

搬运自官网:https://www.typescriptlang.org/docs/handbook/2/types-from-types.html

Typescript’s type system is very powerful (< strong > important < / strong >) because it allows other types to express types. The simplest form of this idea is generics, and we actually have a variety of type operators to use. We can also express types with values we already have.

I generic paradigm

Generic types in typescript are similar to those in other languages. They can be used for both functions and classes. Examples:

 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));

When you create a factory using generics in typescript, you need to reference the class type through its constructor. For example,

 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

Gets a collection of all keys of type

 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

Typeof has two functions in typescript:

  • Gets the type of data
  • Type of captured data
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';

III Index access type

We can use the index access type to find a specific attribute of another type:

The index type itself is a type, so we can completely use union, or other types:

keyof

If you try to index a property that does not exist, you may even see an error:

Property 'alve' does not exist on type 'Person'.

IV Condition type

The form of condition type is a bit like the condition expression () in javascript:

condition ? trueExpression : falseExpression

SomeType extends OtherType ? TrueType : FalseType;

When the type on the left can be assigned to the type on the right, you will get the type in the first branch (“true” branch); Otherwise, you get the type in the latter branch (the “false” branch).

extends

From the above example, the condition type may not be immediately useful – we can tell ourselves whether to choose or! But the power of conditional types is to use them with generics.

Dog extends Animal
number
string

Example:

 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);

Condition type constraint

The following example lets the typescript template know that the incoming type has a property called 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 in condition type

We just found ourselves using conditional types to apply constraints and then extract types. This eventually becomes a common operation, and condition types make it easier.

Conditional types provide us with a way to infer the types we compare in real branches using keywords. For example, we can infer the element type instead of getting it “manually” using the index access type:

infer
Flatten
1 type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

Here, we use keywords to declaratively introduce a new generic type variable instead of specifying how to retrieve the element type in the real branch. This makes it unnecessary for us to consider how to mine and explore the structure of the types we are interested in.

infer
Item
T

We can use keywords to write some useful auxiliary type aliases. For example, for simple cases, we can extract the return type from the function type:

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[]>;

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferring is based on the last signature (this may be the most relaxed and all inclusive case). Overload resolution cannot be performed based on parameter type list.

V mapping type

When you don’t want to repeat yourself, sometimes one type needs to be based on another. The mapping type is based on the syntax of the index signature and is used to declare attribute types that are not declared in advance:

Example: optionsflags will get all attribute types from the type and change their values to bool type

 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 // }

In typescript 4.1 and later, you can remap keys in mapping types using clauses in mapping types:

as
 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 // }

Vi Template text type

The template text type is based on the string text type and can be extended into many strings by union.

They have the same syntax as template literal strings in JavaScript, but are used for type positions. When used with a specific text type, the template text generates a new string text type by connecting the content.

When unions are used at interpolation locations, the type is a collection of each possible string literal that can be represented by each union member. For each interpolation position in the template text, the union is cross multiplied:

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"

Reasoning using template text

Note that we did not benefit from all the information provided in the original delivery object. Given a change (that is, an event), we should expect the callback to receive a parameter of type. Similarly, the changed callback should receive a parameter. We naively use the input ‘parameter. Similarly, the template text type ensures that the data type of the attribute is the same as the first parameter type of the callback of the attribute.

firstName
firstNameChanged
string
age
number
any
callBack

The key insight that makes this possible is that we can use functions with generics, such as:

  • The text used in the first parameter is captured as a text type
  • The literal type can be verified in the union of valid attributes in the generic type
  • You can use indexed access to find the type of a validated property in a generic structure
  • You can then apply this type information to ensure that the parameters of the callback function are of the same type
 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 })

Here we make a generic method.

on

When a user calls with a string, typescript attempts to infer To do this, it matches the previous content and infers the string. Once the typescript determines this, the method can get the type of the original object, as in this case. Similarly, when using a call, typescript finds the type of the property

"firstNameChanged'
Key
Key
"Changed"
"firstName"
on
firstName
string
"ageChanged"
age
number

Reasoning can be combined in different ways, usually deconstructing strings and reconstructing them in different ways.