欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

TypeScript入门笔记 - 泛型约束与泛型类详解 (TS第14章)

最编程 2024-07-27 20:07:03
...

使用 keyof 约束对象

其中使用了 TS 泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字继承 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,它的返回 类型是联合 类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型

function prop<T, K extends keyof T>(obj: T, key: K) {

  return obj[key]

}

 

let o = { a: 1, b: 2, c: 3 }

 

prop(o, 'a')

prop(o, 'd') //,我们需要约束一下这个o里面并没有的东西,此时就会报错发现找不到

//通过提示,我门可以看到类型"d"的参数不能赋给类型"a"|"b"|"c"的参数

泛型类

声明方法跟函数类似名称后面定义 <类型>

使用的时候确定类型 new Sub<number>()

//定义泛型的一个类

class Sub<T>{

   attr:T[] = []//这里的:只是普通的:

   add(a:T):T[]{

       return [a]

   }

}

let s = new Sub<number>()//这里已经使用泛型固定为number了

s.attr = [123]//正常运行

s.attr = ['123']//报错

s.add(123)//也是只能传数字

let str = new Sub<string>()//这里已经使用泛型固定为number了

str.attr = [123]//报错

str.attr = ['123']//正常运行

str.add('123')//也是只能传字符串

console.log(s,str)

泛型工具类型(大量补充额外内容)

作者使用了Typora作为写笔记的编辑器,这里可以对目录进行折叠方面我们查阅我们想要的部分

网络异常,图片无法展示
|

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者可以更好的学习其它的工具类型。

1.typeof

typeof 的主要用途是在类型上下文中获取变量或者属性的类型,下面我们通过一个具体示例来理解一下。

interface Person {

 name: string;

 age: number;

}

const sem: Person = { name: "semlinker", age: 30 };

type Sem = typeof sem; // type Sem = Person

在上面代码中,我们通过 typeof 操作符获取 sem 变量的类型并赋值给 Sem 类型变量,之后我们就可以使用 Sem 类型:

const lolo: Sem = { name: "lolo", age: 5 }

你也可以对嵌套对象执行相同的操作:

const Message = {

   name: "jimmy",

   age: 18,

   address: {

     province: '四川',

     city: '成都'  

   }

}

type message = typeof Message;

/*

type message = {

   name: string;

   age: number;

   address: {

       province: string;

       city: string;

   };

}

*/

此外,typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型,比如:

function toArray(x: number): Array<number> {

 return [x];

}

type Func = typeof toArray; // -> (x: number) => number[]

2.keyof

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

interface Person {

 name: string;

 age: number;

}

type K1 = keyof Person; // "name" | "age"

type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"

type K3 = keyof { [x: string]: Person };  // string | number

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {

 // 字符串索引 -> keyof StringArray => string | number

 [index: string]: string;

}

interface StringArray1 {

 // 数字索引 -> keyof StringArray1 => number

 [index: number]: string;

}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

keyof 也支持基本数据类型:

let K1: keyof boolean; // let K1: "valueOf"

let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...

let K3: keyof symbol; // let K1: "valueOf"

keyof 的作用

JavaScript 是一种高度动态的语言。有时在静态类型系统中捕获某些操作的语义可能会很棘手。以一个简单的 prop 函数为例:

function prop(obj, key) {

 return obj[key];

}

该函数接收 obj 和 key 两个参数,并返回对应属性的值。对象上的不同属性,可以具有完全不同的类型,我们甚至不知道 obj 对象长什么样。

那么在 TypeScript 中如何定义上面的 prop 函数呢?我们来尝试一下:

function prop(obj: object, key: string) {

 return obj[key];

}

在上面代码中,为了避免调用 prop 函数时传入错误的参数类型,我们为 obj 和 key 参数设置了类型,分别为 {}string 类型。然而,事情并没有那么简单。针对上述的代码,TypeScript 编译器会输出以下错误信息:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.

元素隐式地拥有 any 类型,因为 string 类型不能被用于索引 {} 类型。要解决这个问题,你可以使用以下非常暴力的方案:

function prop(obj: object, key: string) {

 return (obj as any)[key];

}

很明显该方案并不是一个好的方案,我们来回顾一下 prop 函数的作用,该函数用于获取某个对象中指定属性的属性值。因此我们期望用户输入的属性是对象上已存在的属性,那么如何限制属性名的范围呢?这时我们可以利用本文的主角 keyof 操作符:

function prop<T extends object, K extends keyof T>(obj: T, key: K) {

 return obj[key];

}

在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型。 是骡子是马拉出来遛遛就知道了,我们来实际测试一下:

type Todo = {

 id: number;

 text: string;

 done: boolean;

}

const todo: Todo = {

 id: 1,

 text: "Learn TypeScript keyof",

 done: false

}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {

 return obj[key];

}

const id = prop(todo, "id"); // const id: number

const text = prop(todo, "text"); // const text: string

const done = prop(todo, "done"); // const done: boolean

很明显使用泛型,重新定义后的 prop<T extends object, K extends keyof T>(obj: T, key: K) 函数,已经可以正确地推导出指定键对应的类型。那么当访问 todo 对象上不存在的属性时,会出现什么情况?比如:

const date = prop(todo, "date");

对于上述代码,TypeScript 编译器会提示以下错误:

Argument of type '"date"' is not assignable to parameter of type '"id" | "text" | "done"'.

这就阻止我们尝试读取不存在的属性。

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {

 [p in Keys]: any

} // -> { a: any, b: any, c: any }

4.infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (

 ...args: any[]

) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface Lengthwise {

 length: number;

}

function loggingIdentity<T extends Lengthwise>(arg: T): T {

 console.log(arg.length);

 return arg;

}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含 length 属性:

loggingIdentity({length: 10, value: 3});

索引类型

在实际开发中,我们经常能遇到这样的场景,在对象中获取一些属性的值,然后建立对应的集合。

let person = {

   name: 'musion',

   age: 35

}

function getValues(person: any, keys: string[]) {

   return keys.map(key => person[key])

}

console.log(getValues(person, ['name', 'age'])) // ['musion', 35]

console.log(getValues(person, ['gender'])) // [undefined]

在上述例子中,可以看到 getValues (persion, ['gender']) 打印出来的是 [undefined],但是 ts 编译器并没有给出报错信息,那么如何使用 ts 对这种模式进行类型约束呢?这里就要用到了索引类型,改造一下 getValues 函数,通过 索引类型查询索引访问 操作符:

function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] {

 return keys.map(key => person[key]);

}

interface Person {

   name: string;

   age: number;

}

const person: Person = {

   name: 'musion',

   age: 35

}

getValues(person, ['name']) // ['musion']

getValues(person, ['gender']) // 报错:

// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.

// Type "gender" is not assignable to type "name" | "age".

编译器会检查传入的值是否是 Person 的一部分。通过下面的概念来理解上面的代码:

T[K]表示对象T的属性K所表示的类型,在上述例子中,T[K][] 表示变量T取属性K的值的数组

// 通过[]索引类型访问操作符, 我们就能得到某个索引的类型

class Person {

   name:string;

   age:number;

}

type MyType = Person['name'];  //Person中name的类型为string type MyType = string

介绍完概念之后,应该就可以理解上面的代码了。首先看泛型,这里有 T 和 K 两种类型,根据类型推断,第一个参数 person 就是 person,类型会被推断为 Person。而第二个数组参数的类型推断(K extends keyof T),keyof 关键字可以获取 T,也就是 Person 的所有属性名,即 ['name', 'age']。而 extends 关键字让泛型 K 继承了 Person 的所有属性名,即 ['name', 'age']。这三个特性组合保证了代码的动态性和准确性,也让代码提示变得更加丰富了

getValues(person, ['gender']) // 报错:

// Argument of Type '"gender"[]' is not assignable to parameter of type '("name" | "age")[]'.

// Type "gender" is not assignable to type "name" | "age".

映射类型

根据旧的类型创建出新的类型,我们称之为映射类型

比如我们定义一个接口

interface TestInterface{

   name:string,

   age:number

}

我们把上面定义的接口里面的属性全部变成可选

// 我们可以通过+/-来指定添加还是删除

type OptionalTestInterface<T> = {

 [p in keyof T]+?:T[p]

}

type newTestInterface = OptionalTestInterface<TestInterface>

// type newTestInterface = {

//    name?:string,

//    age?:number

// }

比如我们再加上只读

type OptionalTestInterface<T> = {

+readonly [p in keyof T]+?:T[p]

}

type newTestInterface = OptionalTestInterface<TestInterface>

// type newTestInterface = {

//   readonly name?:string,

//   readonly age?:number

// }

由于生成只读属性和可选属性比较常用,所以 TS 内部已经给我们提供了现成的实现 Readonly / Partial, 会面内置的工具类型会介绍.

内置的工具类型

Partial

Partial<T> 将类型的属性变成可选

定义

type Partial<T> = {

 [P in keyof T]?: T[P];

};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值的类。中间的 ? 号,用于将所有属性变为可选。

举例说明

interface UserInfo {

   id: string;

   name: string;

}

// error:Property 'id' is missing in type '{ name: string; }' but required in type 'UserInfo'

const xiaoming: UserInfo = {

   name: 'xiaoming'

}

使用  Partial<T>

type NewUserInfo = Partial<UserInfo>;

const xiaoming: NewUserInfo = {

   name: 'xiaoming'

}

这个  NewUserInfo 就相当于

interface NewUserInfo {

   id?: string;

   name?: string;

}

但是 Partial<T> 有个局限性,就是只支持处理第一层的属性,如果我的接口定义是这样的

interface UserInfo {

   id: string;

   name: string;

   fruits: {

       appleNumber: number;

       orangeNumber: number;

   }

}

type NewUserInfo = Partial<UserInfo>;

// Property 'appleNumber' is missing in type '{ orangeNumber: number; }' but required in type '{ appleNumber: number; orangeNumber: number; }'.

const xiaoming: NewUserInfo = {

   name: 'xiaoming',

   fruits: {

       orangeNumber: 1,

   }

}

可以看到,第二层以后就不会处理了,如果要处理多层,就可以自己实现

DeepPartial

type DeepPartial<T> = {

    // 如果是 object,则递归类型

   [U in keyof T]?: T[U] extends object

     ? DeepPartial<T[U]>

     : T[U]

};

type PartialedWindow = DeepPartial<T>; // 现在T上所有属性都变成了可选啦

Required

Required 将类型的属性变成必选

定义

type Required<T> = {

   [P in keyof T]-?: T[P]

};

其中 -? 是代表移除 ? 这个 modifier 的标识。再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly<T> 这个类型

type Readonly<T> = {

   readonly [p in keyof T]: T[p];

}

Readonly

Readonly<T> 的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。

定义

type Readonly<T> = {

readonly [P in keyof T]: T[P];

};

举例说明

interface Todo {

title: string;

}

const todo: Readonly<Todo> = {

title: "Delete inactive users"

};

todo.title = "Hello"; // Error: cannot reassign a readonly property

Pick

Pick 从某个类型中挑出一些属性出来

定义

type Pick<T, K extends keyof T> = {

   [P in K]: T[P];

};

举例说明

interface Todo {

 title: string;

 description: string;

 completed: boolean;

}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {

 title: "Clean room",

 completed: false,