0
点赞
收藏
分享

微信扫一扫

【TypeScript】深入学习TypeScript类型操作


目录

  • ​​前言​​
  • ​​1、泛型​​
  • ​​📌泛型类型​​
  • ​​📌泛型类​​
  • ​​📌泛型约束​​
  • ​​📌在泛型中使用类类型​​
  • ​​2、keyof类型操作符​​
  • ​​3、typeof类型操作符​​
  • ​​4、索引访问类型​​
  • ​​5、条件类型​​
  • ​​📌配合泛型使用​​
  • ​​📌分布式条件类型​​
  • ​​6、映射类型​​
  • ​​📌映射修改器​​
  • ​​📌通过as做key重映射​​
  • ​​📌进一步探索​​
  • ​​结语​​

前言

最近博主一直在创作​​TypeScript​​​的内容,所有的​​TypeScript​​​文章都在我的​​TypeScript专栏​​里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅

​TypeScript​​的类型系统允许用其他类型的术语来表达类型。

通过结合各种类型操作符,我们可以用一种简洁、可维护的方式来表达复杂的操作和值。在本篇文章中,我们将介绍用现有的类型或值来表达一个新类型的方法:

  • 泛型型:带参数的类型
  • Keyof 类型操作符:​​keyof​​ 操作符创建新类型
  • Typeof 类型操作符: 使用​​typeof​​ 操作符来创建新的类型
  • 索引访问类型:使用​​Type['a']​​ 语法来访问一个类型的子集
  • 条件类型:在类型系统中像if语句一样行事的类型
  • 映射类型:通过映射现有类型中的每个属性来创建类型
  • 模板字面量类型:通过模板字面字符串改变属性的映射类型

1、泛型

在​​TypeScript专栏​​中的前几篇文章中,我们以及大致了解了泛型的基本使用,见:

  • ​​【TypeScript】深入学习TypeScript对象类型​​中泛型对象类型
  • ​​【TypeScript】深入学习TypeScript函数​​中泛型函数(通用函数)

在这一节中我们将对泛型进行进一步的补充

📌泛型类型

在​​【TypeScript】深入学习TypeScript函数​​泛型函数(通用函数) 中我们创建了在一系列类型上工作的通用函数,在这一节中,我们将探讨函数本身的类型以及如何创建通用接口

泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:

  • 泛型函数的类型格式:​​<Type>(param:TypeToParamType) => TypeToReturnType​
  • 普通函数类型格式:​​(param:paramType) => returnType​

先看一个我们之前定义过的一个通用函数:

function getFirstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}

它的类型就是​​<Type>(arr: Type[]) => Type | undefined​​​,我们可以将它赋值给同类型的函数​​fn​​:

let fn: <Type>(arr: Type[]) => Type | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));

我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致即可:

let fn: <FnType>(fnArr: FnType[]) => FnType | undefined = getFirstElement;
console.log(fn<number>([1, 2, 3]));

我们也可以把泛型写成一个对象字面类型的调用签名:

let fn: { <FnType>(fnArr: FnType[]): FnType | undefined } = getFirstElement;
console.log(fn<number>([1, 2, 3]));

这时可以将对象字面类型移到一个接口中:

interface Ifn {
<FnType>(fnArr: FnType[]): FnType | undefined;
}
let fn: Ifn = getFirstElement;
console.log(fn<number>([1, 2, 3]));

  • 在一些情况下,我们还可以将通用参数移到整个接口的参数上,这使得我们可以看到我们的泛型是什么类型(例如​​Ifn<string>​​而不仅仅是​​Ifn​​),使得类型参数对接口的所有其它成员可见:

interface Ifn<FnType> {
(fnArr: FnType[]): FnType | undefined;
}
let strFn: Ifn<string> = getFirstElement;
console.log(strFn(["1", "2", "3"]));
console.log(strFn([1, 2, 3])); // err:不能将类型“number”分配给类型“string”

注意:这里的例子已经变了,不再是简单的将​​getFirstElement​​​函数直接赋值给另一个函数,而是将类型参数为​​string​​​的​​getFirstElement​​​函数赋值给​​strFn​

上述​​strFn​​​相当于​​fn<string>​​:

interface Ifn {
<FnType>(fnArr: FnType[]): FnType | undefined;
}
let fn: Ifn = getFirstElement;
console.log(fn<string>(["1", "2", "3"]));

📌泛型类

泛型类在类的名字后面有一个角括号(​​<>​​)中的泛型参数列表:

class Add<AddType> {
initVal: AddType| undefined;
add: ((x: AddType, y: AddType) => AddType) | undefined;
}

使用:

let myNumber = new Add<number>();
myNumber.initVal = 1;
myNumber.add = function (x, y) {
return x + y;
};
console.log(myNumber.add(myNumber.initVal, 18)); // 19

let myStr = new Add<string>();
myStr.initVal = "Ailjx";
myStr.add = function (x, y) {
return x + y;
};
console.log(myStr.add(myStr.initVal, " OK")); // Ailjx OK

就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。

注意:一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数

📌泛型约束

在​​【TypeScript】深入学习TypeScript函数​​泛型函数(通用函数) 中我们已经了解过了使用​​extends​​约束泛型,这一节我们继续深入

在泛型约束中使用类型参数

你可以声明一个受另一个类型参数约束的类型参数。

例如,我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 ​​obj​​ 上的属性,所以我们要在这两种类型之间放置一个约束条件:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}

​keyof​​ 运算符接收一个对象类型,并产生其键的字符串或数字字面联合,下面会详细讲解

【TypeScript】深入学习TypeScript类型操作_typescript

📌在泛型中使用类类型

在​​TypeScript​​中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型,比如说:

function create<Type>(c: new () => Type): Type {
return new c();
}

​create​​函数代表接收一个构造函数,并返回其实例

参数​​c​​的类型使用的是构造签名,表示其接收一个构造函数,并且该构造函数实例的类型(​​Type​​​)被当作了​​create​​​函数的类型参数并在其它地方进行使用,如​​create​​​的返回值类型就是引用了​​Type​

一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系:

class Animal {
numLegs: number = 4;
}

class Bee extends Animal {
name: string = "Bee";
getName() {
console.log(this.name);
}
}

class Lion extends Animal {
name: string = "Lion";
getName() {
console.log(this.name);
}
}

function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}

createInstance(Bee).getName(); // Bee
createInstance(Lion).getName(); // Lion

这里的​​createInstance​​​函数表示只能接收一个实例类型受限于​​Animal​​的构造函数,并返回其实例

【TypeScript】深入学习TypeScript类型操作_泛型_02

2、keyof类型操作符

​keyof​​ 运算符接收一个对象类型,并产生其的字符串或数字字面联合:

type ObjType = { x: number; y: number };

const p1: keyof ObjType = "x";
// 相当于
// const p1: "x" | "y" = "x";

如果该类型有一个字符串或数字索引签名, ​​keyof​​ 将返回这些类型:

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // A为 number
const a: A = 1;

type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // M为 string|number
const m: M = "a";
const m2: M = 10;

注意:在这个例子中, ​​M​​​ 是 ​​string|number​​​ ——这是因为​​JavaScript​​​对象的键总是被强制为字符串,所以 ​​obj[0]​​​ 总是与​​obj["0"]​​ 相同。

3、typeof类型操作符

在​​JavaScript​​​中可以使用​​typeof​​​操作符获取某一变量的类型,在​​TypeScript​​中我们可以使用它来在类型上下文中引用一个变量或属性的类型:

let s = "hello";
let n: typeof s; // n类型为string
n = "world";
n = 100; // err:不能将类型“number”分配给类型“string”

结合其他类型操作符,你可以使用​​typeof​​来方便地表达许多模式。

例如我们想要获取函数返回值的类型:

  • ​TypeScript​​​中内置的类型​​ReturnType<T>​​ 接收一个函数类型并产生其返回类型:

type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; // k为boolean

  • 如果直接在一个函数名上使用​​ReturnType​​ ,我们会看到一个指示性的错误:
  • 【TypeScript】深入学习TypeScript类型操作_泛型_03

  • 为了指代值​​f​​的类型,我们使用 ​​typeof​​ :

function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>; // P为{ x: number, y: number }

只有在标识符(即变量名)或其属性上使用​​typeof​​是合法的

4、索引访问类型

可以使用一个索引访问类型来查询一个类型上的特定属性的类型:

type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // Age类型为number

还可以配合联合类型​unions​​​、​​keyof​​ 或者其他类型进行使用:

interface Person {
name: string;
age: number;
alive: boolean;
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11: I1 = 100;
const i12: I1 = "";

// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21: I2 = "";
const i22: I2 = 100;
const i23: I2 = false;

将索引访问类型和​​typeof​​​ ,​​number​​ 结合起来,方便地获取一个数组字面的元素类型:

const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];

/* type Person = {
name: string;
age: number;
} */
type Person = typeof MyArray[number];
const p: Person = {
name: "xiaoqian",
age: 11,
};

// type Age = number
type Age = typeof MyArray[number]["age"];
const age: Age = 11;

// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2: Age2 = 11;

注意:

  • 在索引时只能使用类型引用,不能使用变量引用:
  • 【TypeScript】深入学习TypeScript类型操作_typescript_04

  • 可以使用类型别名来实现类似风格的重构:

type key = "age";
type Age = Person[key];

5、条件类型

在​​TypeScript​​我们可以使用三元表达式来判断一个类型:

interface Animal {}
interface Dog extends Animal {}

// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;

条件类型表达式是通过​​extends​​进行约束和判断

📌配合泛型使用

先看一个简单的例子:

type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;

当 ​​Flatten​​ 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取数组的元素类型。否则,它只是返回它被赋予的类型。

在看一个复杂的例子,实现一个获取​​id​​​或​​name​​​的对象格式的函数​​getIdOrNameObj​​:

interface IId {
id: number;
}
interface IName {
name: string;
}
// 条件类型配合泛型对类型进行判断和选择
type IdOrName<T extends number | string> = T extends number ? IId : IName;

function getIdOrNameObj<T extends number | string>(idOrName: T): IdOrName<T> {
if (typeof idOrName === "number") {
return {
id: idOrName,
} as IdOrName<T>;
} else {
return {
name: idOrName,
} as IdOrName<T>;
}
}

const myId = getIdOrNameObj(1); // myId类型为IId
const myName = getIdOrNameObj("Ailjx"); // myName类型为IName
```### 类型推理
在条件类型的 `extends`子句中我们可以使用 `infer` 声明来推断元素类型
> `infer` 声明只能在条件类型的 `extends`子句中使用

例如,我们使用`infer` 关键字来改写上面的`Flatten`:

```typescript
type Flatten<T> = T extends Array<infer Item> ? Item : T;

// type Str = string
type Str = Flatten<string[]>;
// type Str = number
type Num = Flatten<number[]>;

这里使用 ​​infer​​​ 关键字来声明性地引入一个名为 ​​Item​​ 的新的通用类型变量

这里​​infer Item​​​相当于一个占位,它暂时代表​​Array​​​中元素的类型,当​​Flatten​​​类型参数被赋值为数组后,​​TypeScript​​​就会自动推断出​​extends​​​语句中​​Array​​​中元素的类型,这时​​infer Item​​​这个占位就指向了数组元素的类型,之后就能直接使用​​Item​​来代指数组元素的类型了

这使得我们不用再使用索引访问类型​​T[number]​​ "手动 "提取数组元素的类型了

使用 ​​infer​​ 关键字从函数类型中提取出返回类型:

// 当GetReturnType接收类型为函数签名时返回函数返回值类型,否者直接返回接收的类型
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: Type;

// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

// type Arr=any[]
type Arr = GetReturnType<any[]>;

当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断:

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;

​declare​​​可以向​​TypeScript​​域中引入一个变量,这可以解决在重载函数只有重载签名而没有实现签名时的报错

📌分布式条件类型

当条件类型作用于一个通用类型时,当给定一个联合类型时,它就变成了分布式的:

type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;

将一个联合类型​​string | number​​​插入​​ToArray​​,那么条件类型将被应用于该联合的每个成员

  • ​StrArrOrNumArr​​​分布在​​string | number​​;
  • 条件类型会对联合的每个成员类型进行映射:​​ToArray<string> | ToArray<number>​
  • 最终返回​​string[] | number[]​

取消分布式:

如果不需要分布式的这种行为,我们可以使用方括号​[]​​​包围​​extends​​关键字的两边

type ToArray<Type> = [Type] extends [any] ? Type[] : never;
// type StrArrOrNumArr = (string|number)[]
type StrArrOrNumArr = ToArray<string | number>;

6、映射类型

当一个类型可以以另一个类型为基础创建新类型。

映射类型建立在索引签名(见【​​TypeScript】深入学习TypeScript对象类型​​)的语法上

映射类型是一种通用类型,它使用 ​​PropertyKeys​​​ 的联合(经常通过​​keyof​​ 创建)迭代键来创建一个类型:

type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};

在这个例子中, ​​OptionsFlags​​​ 将从​​Type​​​ 类型中获取所有属性,并将它们的值的类型改为​​boolean​​:

type Obj = {
name: string;
age: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
name: boolean;
age: boolean;
}
*/

📌映射修改器

在映射过程中,有两个额外的修饰符可以应用: ​​readonly​​​ 和​​?​​​ ,它们分别影响可变性和可选性,可以通过用​​-​​​ 或​​+​​​ 作为前缀来删除或添加这些修饰符(不加修饰符就默认是​​+​​ ):

type OptionsFlags<Type> = {
// 删除readonly和?,readonly在前,?在后
-readonly [Property in keyof Type]-?: boolean;
};
type Obj = {
readonly name: string;
age?: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
name: boolean;
age: boolean;
}
*/

📌通过as做key重映射

在​​TypeScript 4.1​​​及以后的版本中,可以通过映射类型中的​​as​​子句修改映射类型中的键名:

type OptionsFlags<Type> = {
// 将键重命名为哦、
[Property in keyof Type as "o"]: Type[Property];
};
type Obj = {
name: string;
age: number;
};

type FeatureOptions = OptionsFlags<Obj>;
/*
type FeatureOptions = {
o:string|number
}
*/

上面是将所有键名都更改成了​​'o'​

我们也可以利用​​模板字面类型​​,在之前的属性名称的基础上进行更改:

type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property];
};
interface Person {
name: string;
age: number;
location: string;
}
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;

  • ​Capitalize​​​ 为​​TS​​内置类型,能将传入的字符串类型首字母转为大写
  • ​string & Property​​​通过交叉类型,确保​​Capitalize​​ 接收的为字符串类型

可以通过条件类型Exclude根据键名产生​never​ ,从而过滤掉该键:

type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property];
};

interface Circle {
kind: "circle";
radius: number;
}

/*
type KindlessCircle = {
radius: number;
}
*/
type KindlessCircle = RemoveKindField<Circle>;

​Exclude​​​为​​TS​​​内置类型:​​type Exclude<T, U> = T extends U ? never : T​

可以映射任意的联合体:

type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
};
type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
type Config = EventConfig<SquareEvent | CircleEvent>;

📌进一步探索

映射类型与本篇文章中指出的其他功能配合得很好,例如,下面这个使用条件类型的映射类型,它根据一个对象类型的属性​​show​​​是否被设置为字面类型​​true​​​而返回​​true​​​或​​false​​:

type ExtractShow<Type> = {
[Property in keyof Type]: Type[Property] extends { show: true }
? true
: false;
};

type PermissionInfo = {
home: { url: string; show: true };
about: { url: string; show: true };
admin: { url: string };
};

/*
type judge = {
home: true;
about: true;
admin: false;
}
*/
type judge = ExtractShow<PermissionInfo>;

结语

至此,本篇文章内容就全部结束了,如果本篇文章对你有所帮助,还请客官一件四连!

举报

相关推荐

0 条评论