一文读懂 TypeScript 泛型及应用

length: number;

}

function identity(arg: T): T {

console.log(arg.length); // 可以获取length属性

return arg;

}

T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。之后,当我们使用不含有 length 属性的对象作为参数调用  identity 函数时,TypeScript 会提示相关的错误信息:

identity(68); // Error

// Argument of type ‘68’ is not assignable to parameter of type ‘Length’.(2345)

此外,我们还可以使用 , 号来分隔多种约束类型,比如:<T extends Length, Type2, Type3>。而对于上述的 length 属性问题来说,如果我们显式地将变量设置为数组类型,也可以解决该问题,具体方式如下:

function identity(arg: T[]): T[] {

console.log(arg.length);

return arg;

}

// or

function identity(arg: Array): Array {

console.log(arg.length);

return arg;

}

4.2 检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。 “耳听为虚,眼见为实”,我们来举个 keyof 的使用示例:

interface Person {

name: string;

age: number;

location: string;

}

type K1 = keyof Person; // “name” | “age” | “location”

type K2 = keyof Person[];  // number | “length” | “push” | “concat” | …

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

通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {

return obj[key];

}

在以上的 getProperty 函数中,我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 不同。

下面我们来看一下如何使用 getProperty 函数:

enum Difficulty {

Easy,

Intermediate,

Hard

}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {

return obj[key];

}

let tsInfo = {

name: “Typescript”,

supersetOf: “Javascript”,

difficulty: Difficulty.Intermediate

}

let difficulty: Difficulty =

getProperty(tsInfo, ‘difficulty’); // OK

let supersetOf: string =

getProperty(tsInfo, ‘superset_of’); // Error

在以上示例中,对于 getProperty(tsInfo, 'superset_of') 这个表达式,TypeScript 编译器会提示以下错误信息:

Argument of type ‘“superset_of”’ is not assignable to parameter of type

‘“difficulty” | “name” | “supersetOf”’.(2345)

很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。接下来,我们来介绍一下泛型参数默认类型。

五、泛型参数默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。

泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T=Default Type>,对应的使用示例如下:

interface A<T=string> {

name: T;

}

const strA: A = { name: “Semlinker” };

const numB: A = { name: 101 };

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。

  • 必选的类型参数不能在可选的类型参数后。

  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。

  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。未指定的类型参数会被解析为它们的默认类型。

  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。

  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。

  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型。

六、泛型条件类型

在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。

条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

T extends U ? X : Y

以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:

interface Dictionary<T = any> {

}

type StrDict = Dictionary

type DictMember = T extends Dictionary ? V : never

type StrDictMember = DictMember // string

在上面示例中,当类型 T 满足 T extends Dictionary 约束时,我们会使用 infer关键字声明了一个类型变量 V,并返回该类型,否则返回 never 类型。

在 TypeScript 中,never 类型表示的是那些永不存在的值的类型。例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

另外,需要注意的是,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。即使 any 也不可以赋值给 never

除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:

async function stringPromise() {

return “Hello, Semlinker!”;

}

interface Person {

name: string;

age: number;

}

async function personPromise() {

return { name: “Semlinker”, age: 30 } as Person;

}

type PromiseType = (args: any[]) => Promise;

type UnPromisify = T extends PromiseType ? U : never;

type extractStringPromise = UnPromisify; // string

type extractPersonPromise = UnPromisify; // Person

七、泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍其中几个常用的工具类型。

7.1 Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

/**

* node_modules/typescript/lib/lib.es5.d.ts

* Make all properties in T optional

*/

type Partial = {

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

};

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

示例:

interface Todo {

title: string;

description: string;

}

function updateTodo(todo: Todo, fieldsToUpdate: Partial) {

return { …todo, …fieldsToUpdate };

}

const todo1 = {

title: “organize desk”,

description: “clear clutter”

};

const todo2 = updateTodo(todo1, {

description: “throw out trash”

});

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{

title?: string | undefined;

description?: string | undefined;

}

7.2 Record

Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。

定义:

/**

* node_modules/typescript/lib/lib.es5.d.ts

* Construct a type with a set of properties K of type T

*/

type Record<K extends keyof any, T> = {

};

示例:

interface PageInfo {

title: string;

}

type Page = “home” | “about” | “contact”;

const x: Record<Page, PageInfo> = {

about: { title: “about” },

contact: { title: “contact” },

home: { title: “home” }

};

7.3 Pick

Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

定义:

// node_modules/typescript/lib/lib.es5.d.ts

/**

* From T, pick a set of properties whose keys are in the union K

*/

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

};

示例:

interface Todo {

title: string;

description: string;

completed: boolean;

}

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

const todo: TodoPreview = {

title: “Clean room”,

completed: false

};

7.4 Exclude

Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。

定义:

// node_modules/typescript/lib/lib.es5.d.ts

/**

* Exclude from T those types that are assignable to U

*/

type Exclude<T, U> = T extends U ? never : T;

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

示例:

type T0 = Exclude<“a” | “b” | “c”, “a”>; // “b” | “c”

type T1 = Exclude<“a” | “b” | “c”, “a” | “b”>; // “c”

type T2 = Exclude<string | number | (() => void), Function>; // string | number

7.5 ReturnType

ReturnType<T> 的作用是用于获取函数 T 的返回类型。

定义:

// node_modules/typescript/lib/lib.es5.d.ts

/**

* Obtain the return type of a function type

*/

type ReturnType<T extends (…args: any) => any> = T extends (…args: any) => infer R ? R : any;

示例:

type T0 = ReturnType<() => string>; // string

type T1 = ReturnType<(s: string) => void>; // void

type T2 = ReturnType<() => T>; // {}

type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]

type T4 = ReturnType; // any

type T5 = ReturnType; // any

type T6 = ReturnType; // Error

type T7 = ReturnType; // Error

简单介绍了泛型工具类型,最后我们来介绍如何使用泛型来创建对象。

八、使用泛型创建对象

8.1 构造签名

有时,泛型类可能需要基于传入的泛型 T 来创建其类型相关的对象。比如:

class FirstClass {

id: number | undefined;

}

class SecondClass {

name: string | undefined;

}

class GenericCreator {

create(): T {

return new T();

}

}

const creator1 = new GenericCreator();

const firstClass: FirstClass = creator1.create();

const creator2 = new GenericCreator();

const secondClass: SecondClass = creator2.create();

在以上代码中,我们定义了两个普通类和一个泛型类 GenericCreator<T>。在通用的 GenericCreator 泛型类中,我们定义了一个名为 create 的成员方法,该方法会使用 new 关键字来调用传入的实际类型的构造函数,来创建对应的对象。但可惜的是,以上代码并不能正常运行,对于以上代码,在 TypeScript v3.9.2 编译器下会提示以下错误:

‘T’ only refers to a type, but is being used as a value here.

这个错误的意思是:T 类型仅指类型,但此处被用作值。那么如何解决这个问题呢?根据 TypeScript 文档,为了使通用类能够创建 T 类型的对象,我们需要通过其构造函数来引用 T 类型。对于上述问题,在介绍具体的解决方案前,我们先来介绍一下构造签名。

在 TypeScript 接口中,你可以使用 new 关键字来描述一个构造函数:

interface Point {

new (x: number, y: number): Point;

}

以上接口中的 new (x: number, y: number) 我们称之为构造签名,其语法如下:

ConstructSignature:newTypeParametersopt(ParameterListopt)TypeAnnotationopt

在上述的构造签名中,TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分别表示:可选的类型参数、可选的参数列表和可选的类型注解。与该语法相对应的几种常见的使用形式如下:

new C

new C ( … )

new C < … > ( … )

介绍完构造签名,我们再来介绍一个与之相关的概念,即构造函数类型。

8.2 构造函数类型

在 TypeScript 语言规范中这样定义构造函数类型:

An object type containing one or more construct signatures is said to be a constructor type. Constructor types may be written using constructor type literals or by including construct signatures in object type literals.

通过规范中的描述信息,我们可以得出以下结论:

  • 包含一个或多个构造签名的对象类型被称为构造函数类型;

  • 构造函数类型可以使用构造函数类型字面量或包含构造签名的对象类型字面量来编写。

那么什么是构造函数类型字面量呢?构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:

new < T1, T2, … > ( p1, p2, … ) => R

该形式与以下对象类型字面量是等价的:

{ new < T1, T2, … > ( p1, p2, … ) : R }

下面我们来举个实际的示例:

// 构造函数类型字面量

new (x: number, y: number) => Point

等价于以下对象类型字面量:

{

new (x: number, y: number): Point;

}

8.3 构造函数类型的应用

在介绍构造函数类型的应用前,我们先来看个例子:

interface Point {

new (x: number, y: number): Point;

x: number;

y: number;

}

class Point2D implements Point {

readonly x: number;

readonly y: number;

constructor(x: number, y: number) {

this.x = x;

this.y = y;

}

}

const point: Point = new Point2D(1, 2);

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

Class ‘Point2D’ incorrectly implements interface ‘Point’.

Type ‘Point2D’ provides no match for the signature ‘new (x: number, y: number): Point’.

相信很多刚接触 TypeScript 不久的小伙伴都会遇到上述的问题。要解决这个问题,我们就需要把对前面定义的 Point 接口进行分离,即把接口的属性和构造函数类型进行分离:

interface Point {

x: number;

y: number;

}

interface PointConstructor {

new (x: number, y: number): Point;

}

完成接口拆分之后,除了前面已经定义的 Point2D 类之外,我们又定义了一个 newPoint 工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来创建对应的 Point 对象。

class Point2D implements Point {

readonly x: number;

readonly y: number;

constructor(x: number, y: number) {

this.x = x;

this.y = y;

}

}

function newPoint(

pointConstructor: PointConstructor,

x: number,

y: number

): Point {

return new pointConstructor(x, y);

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
er, y: number): Point;

}

完成接口拆分之后,除了前面已经定义的 Point2D 类之外,我们又定义了一个 newPoint 工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来创建对应的 Point 对象。

class Point2D implements Point {

readonly x: number;

readonly y: number;

constructor(x: number, y: number) {

this.x = x;

this.y = y;

}

}

function newPoint(

pointConstructor: PointConstructor,

x: number,

y: number

): Point {

return new pointConstructor(x, y);

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-8oZc9mig-1715802163298)]

[外链图片转存中…(img-LM3RDl50-1715802163298)]

[外链图片转存中…(img-wCrZr8MG-1715802163298)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值