内容接《TypeScript 进阶与实践(一)》,建议在学习完上一篇内容后再继续阅读这一篇。
三、类型编程
3.1 泛型
泛型是一种创建可复用代码组件的工具。泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在使用时作为参数指明这些类型。泛型可以用于定义函数、接口或类,不预先定义好具体的类型,而在使用的时候再指定类型。泛型可以提高代码的可读性和可维护性,同时也可以提高代码的复用性和灵活性。
这部分以函数中如何使用泛型来展开讲解,你还可以在类型别名、类和对象等场景使用泛型,使用方式大同小异,这里就不再赘述。
3.1.1 泛型类型变量
假设要定义一个 reflect
函数 ,它可以接收任意类型的参数,并原封不动地返回参数的值,在没有使用泛型之前,我们可以这样去实现:
function reflect(param: unknown) {
return param;
}
const str = reflect('string'); // str 类型为 unknown
const num = reflect(1); // num 类型为 unknown
你会发现,这种实现方式,不管我们传入的参数是什么类型,最终返回的值的类型都是 unknown
,这样在使用返回值时还需要将类型缩小才能安全的使用,并不是很方便。我们已经知道 reflect
函数会原封不动的返回传入的参数了,那么有没有办法让返回值的类型根据传入的参数的类型变化而变化呢?答案是肯定的,使用泛型类型变量就可以满足这个要求。
泛型类型变量的使用方式如下面示例所示:
function reflect<T>(param: T) {
return param;
}
// TS 会根据参数的值的类型来推断泛型类型参数的类型
let str = reflect('string'); // str 的类型为 string
let num = reflect(1); // num 的类型为 num
// 显式传递泛型类型参数,会限制传入的参数类型必须与传入的泛型参数类型一致
let value = reflect<string>(123); // str 的类型为 string
上面示例中我们在 reflect
函数名称的后面使用 <T>
声明了泛型类型变量 T
,然后将参数 param
的类型指定为了 T
,函数体中原封不动的返回了参数 param
,返回值的类型会被隐式推断为 param
的类型,即返回值的类型也为 T
。
3.1.2 多个泛型类型参数
要定义多个泛型类型参数时,只需要用半角逗号将多个泛型类型参数名隔开即可,如下面这个示例所示:
function genericTypeVariables<T, P>(a: T, b: P) {
return { a, b };
}
3.1.3 泛型类型参数约束
泛型类型参数约束可以限制传入的泛型类型参数的类型,它通过 extends
关键字来实现。使用泛型类型参数约束,可以确保泛型类型参数满足特定的条件,从而提高代码的可读性和可维护性。如下面的示例所示:
function getName<T extends { name: string }>(obj: T) {
return obj.name;
}
let result1 = getName({ name: 'Jason' }); // result1 类型为 string
let result2 = getName({ age: 18 }); // @error: 2345
在这个示例中我们限制了泛型类型参数 T
的类型必须包含属性 name
且属性 name
的类型必须为 string
。在调用该泛型函数时,如果传入的参数的类型不包含 name
属性就会产生错误。
3.1.4 泛型默认值
泛型类型参数还支持设置默认值,以下是一个 TypeScript 泛型参数带默认值的示例:
interface Person {
name: string;
age: number;
}
function identity<O extends Person, R = string>(obj: O, format?: (obj: O) => R): R {
return format ? format(obj) : (obj.name as R);
}
// result1 的类型为泛型类型参数 R 的默认类型 string
const result1 = identity({ name: 'John', age: 30 });
// result2 的类型为 number
const result2 = identity({ name: 'John', age: 30 }, obj => obj.age);
在这个示例中,泛型类型参数 R
带有默认值 string
。如果没有显式地给出类型参数,那么 R
将被推断为 string
类型。在不指定类型参数的情况下调用函数 identity
,且不传入 format
参数时,返回值的类型为泛型类型参数 R
的默认类型 string
,当传入 format
参数,TypeScript 根据传入的参数的类型推断出泛型类型参数 R
的类型为 number
,因此 result2
的类型为 number
。
3.2 keyof 类型运算符
keyof 类型运算符 用于从对象类型中提取键类型。例如,如果我们有一个对象类型:
interface Person {
name: string;
age: number;
address: string;
}
我们可以使用 keyof
来提取它的键类型:
type PersonKeys = keyof Person; // "name" | "age" | "address"
这将返回一个联合类型,包含对象的所有键。这个联合类型可以用于确定对象是否具有某个属性,如下面示例所示:
interface Person {
name: string;
age: number;
location: string;
}
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person: Person = {
name: 'John',
age: 30,
location: 'Seattle',
};
console.log(getProperty(person, 'name')); // John
console.log(getProperty(person, 'age')); // 30
console.log(getProperty(person, 'location')); // Seattle
// @error: Argument of type '"name1"' is not assignable to parameter of type 'keyof Person'.(2345)
console.log(getProperty(person, 'name1'));
3.3 typeof 类型运算符
typeof 类型运算符 用于获取变量或对象的类型。如下面示例所示:
interface Person {
name: string;
age: number;
address: string;
}
const person: Person = {
name: 'John',
age: 30,
address: 'Seattle',
};
type PersonType = typeof person; // { name: string; age: number; address: string; }
当我们导入一个函数,但该函数没有提供其参数和返回值的类型时,我们可以使用 TypeScript 中的 typeof
关键字来提取函数的类型。这是一个简单的示例,演示了如何使用 typeof
关键字来提取函数参数的类型:
import { myFunction } from './myModule';
// Parameters 工具类型可以从一个函数类型中提取函数的参数类型,将在后续章节讲到
type MyFunctionArgs = Parameters<typeof myFunction>;
let args: MyFunctionArgs = ...;
myFunction(...args);
在这个示例中,我们首先从 myModule
模块中导入了一个名为 myFunction
的函数。然后,我们使用 typeof
关键字来获取 myFunction
函数的类型,并使用 Parameters
工具类型来提取该函数的参数类型。最后,我们定义了一个名为 args
的变量,其类型为 MyFunctionArgs
,并将其作为 myFunction
函数的参数传递。
3.4 索引访问类型
索引访问类型 索引访问类型是一种类型操作符,用于获取对象或类型的属性类型。例如,如果我们有一个对象类型:
interface Person {
name: string;
age: number;
alive: boolean;
}
type Age = Person['age']
将返回一个类型,即 Person
对象的 age
属性的类型。我们还可以使用联合类型、keyof
或其他方式来使用索引访问类型。例如:
type I1 = Person['age' | 'name']; // string | number
type I2 = Person[keyof Person]; // string | number | boolean
type AliveOrName = 'alive' | 'name';
type I3 = Person[AliveOrName]; // string | boolean
3.5 条件类型
条件类型是 TypeScript 2.8 版本中引入的一种类型。它可以帮助我们根据输入类型描述输入和输出类型之间的关系。条件类型的形式看起来有点像 JavaScript 中的条件表达式 condition ? trueExpression : falseExpression
:SomeType extends OtherType ? TrueType : FalseType
。当 extends
左边的类型可以分配给右边的类型时,你将获得第一个分支中的类型(TrueType
);否则,您将获得后一个分支中的类型(FalseType
)
这是一个 TypeScript 中条件类型的示例:
type IsNumber<T> = T extends number ? true : false;
const isNumber: IsNumber<42> = true; // isNumber is of type "true"
在这个例子中,我们定义了一个名为 IsNumber
的条件类型,它检查类型 T
是否为 number
类型。如果是,则返回 true
类型,否则返回 false
类型。然后,我们使用这个条件类型来定义一个常量 isNumber
,并将其类型设置为 IsNumber<42>
。由于 42
是一个数字,所以 isNumber
的类型为 true
。
3.2.1 infer
在条件类型中可以使用 infer 关键字 进行类型推断。在 TypeScript 官网使用了 ReturnType
这一经典例子说明它的作用:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
上面这个例子中,如果泛型类型变量 T
继承了 extends (...args: any[]) => any
类型,则返回类型 R
,否则返回 never
。其中类型变量 R
被定义在 extends (...args: any[]) => infer R
中应该标注返回值类型的位置,即 R
是根据传入参数的类型的返回值的类型推导出来的。
3.2.2 分配条件类型
分配条件类型是 TypeScript 中的一种条件类型,它用来表达非均匀类型映射。当传入的类型参数为联合类型时,它们会被分配类型。例如,假设我们有一个名为 ToArray
的类型,它将一个类型转换为数组类型:
type ToArray<Type> = Type extends any ? Type[] : never;
如果我们将联合类型传入 ToArray
,则条件类型将应用于该联合类型的每个成员:
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;
这里 string | number
分别与 ToArray 结合产生了 string[]
和 number[]
组成的联合类型。
3.6 映射类型
映射类型能够将一个类型映射成另一个类型。
例如,假设我们有一个名为 OldType
的类型,它具有三个字符串属性:
type OldType = { a: string; b: string; c: string };
我们可以使用映射类型将 OldType
中的每个属性映射到具有数字值的新类型 NewType
:
// 新类型为 { a: number, b: number, c: number }
type NewType = { [P in keyof OldType]: number };
3.7 模板字面量类型
模板字面量类型是 TypeScript 4.1 开始支持的一种类型。它基于字符串字面量类型,可以展开为多个字符串类型的联合类型。其语法与 JavaScript 中的模板字面量是一致的,但是是用在类型的位置上。
例如,当与某个具体的字面量类型一起使用时,模板字面量会将文本连接从而生成一个新的字符串字面量类型。
type World = 'world';
type Greeting = `hello ${World}`; // 'hello world'
const str1: Greeting = 'hello world';
const str2: Greeting = 'hello'; // @error: Type '"hello"' is not assignable to type '"hello world"'.(2322)
如果在替换字符串的位置是联合类型,那么结果类型是由每个联合类型成员构成的字符串字面量的集合:
type EmailLocaleIDs = 'welcome_email' | 'email_heading';
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff';
// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
const str1: AllLocaleIDs = 'welcome_email_id';
const str2: AllLocaleIDs = 'email_heading_id';
const str3: AllLocaleIDs = 'footer_title_id';
const str4: AllLocaleIDs = 'footer_sendoff_id';
const str5: AllLocaleIDs = 'hello'; // @error: Type '"hello"' is not assignable to type '"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"'.(2322)
模板字面量类型可以用于限制方法的字符串入参的格式、对象的属性命名格式等场景,下面是一个示例:
type PrefixKeys<T, K extends string> = { [P in keyof T & string as `${K}${P}`]: T[P] };
type Example = {
a: number;
b: string;
};
type PrefixedExample = PrefixKeys<Example, 'x-'>;
// 等同于
// type PrefixedExample = {
// 'x-a': number;
// 'x-b': string;
// }
3.8 内置工具类型
TypeScript 内置了一些基本的工具类型,可以直接使用。这些工具类型都定义在 TypeScript 核心库的定义文件中。这些工具类型可以帮助开发者更好地利用基础类型,以免重复造轮子,并能通过这些工具类型实现更高级的类型操作。
3.8.1 接口操作类型
Partial<Type>
Partial
工具类型可以将一个类型的所有属性变为可选的,且该工具类型返回的类型是给定类型的所有子集。如下面示例所示:
interface Person {
name: string;
age: number;
weight?: number;
}
type PartialPerson = Partial<Person>;
// 相当于
// interface PartialPerson {
// name?: string;
// age?: number;
// weight?: number;
// }
一个常见的使用场景是通过使用 Partial 来定义一个函数参数,该函数只接受对象中的一部分属性:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: 'organize desk',
description: 'clear clutter',
};
const todo2 = updateTodo(todo1, { description: 'throw out trash' });
Partial
只会将对象类型的直接子属性设置为可选,如果希望将对象的类型的所有后代属性都设为可选的,你可以实现一个 DeepPartial
工具类型,代码如下:
type DeepPartial<T extends Record<string, any>> = {
[P in keyof T]?: null | DeepPartial<T[P]>;
};
Required<T>
与 Partial
工具类型相反,Required
工具类型可以将给定类型的所有属性变为必选的,如示例所示:
interface Person {
name: string;
age?: number;
weight?: number;
}
type RequiredPerson = Required<Person>;
// 相当于
// interface RequiredPerson {
// name: string;
// age: number;
// weight: number;
// }
ReadOnly<T>
Readonly
工具类型可以将给定类型的所有属性设为只读,这意味着给定类型的属性不可以被重新赋值,如示例所示:
interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: 'Delete inactive users',
};
todo.title = 'Hello'; // @errors: 2540
像 React 中组件的状态、属性以及其它一些使用不可变数据类型的场景下,我们不希望开发者直接修改对象的属性,就可以使用到这个工具类型。
Pick<T, K>
Pick
工具类型可以从给定的类型中选取出指定的键值,然后组成一个新的类型,如示例所示:
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, 'title' | 'completed'>;
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
};
Omit<T, K>
与 Pick
类型相反,Omit
工具类型的功能是返回去除指定的键值之后返回的新类型,如示例所示:
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, 'description'>;
// type TodoPreview = {
// title: string;
// completed: boolean;
// createdAt: number;
// }
type TodoInfo = Omit<Todo, 'completed' | 'createdAt'>;
// type TodoInfo = {
// description: string;
// title: string;
// }
3.8.2 联合类型相关
Exclude<T, U>
Exclude
的作用是从联合类型中去除指定的类型,这里有点类似 Omit
,实际上 Omit
的官方实现就是依赖了 Exclude
,如示例所示:
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
// Omit 实现使用到了 Exclude
type MyOmit<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P];
};
Extract<T, U>
Extract
类型的作用与 Exclude
正好相反,Extract
主要用来从联合类型中提取指定的类型,类似于操作接口类型中的 Pick
类型,如示例所示:
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>; // type T1 = () => void
NonNullable<T>
NonNullable
的作用是从联合类型中去除 null
或者 undefined
的类型,如示例所示:
type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>; // type T1 = () => void
Record<K, T>
Record
的作用是生成接口类型,然后我们使用传入的泛型参数分别作为接口类型的属性和值。如示例所示:
enum TaskStatus {
READY = 'ready',
IN_PROGRESS = 'in_progress',
DONE = 'done',
}
const mapTaskStatusToColor = {
[TaskStatus.READY]: 'yellow',
[TaskStatus.IN_PROGRESS]: 'green',
};
const mapTaskStatusToName: Record<TaskStatus, string> = {
// @error: Property '[TaskStatus.DONE]' is missing...
[TaskStatus.READY]: '待处理',
[TaskStatus.IN_PROGRESS]: '处理中',
};
在这个示例中定义了任务状态的枚举值 TaskStatus
,然后使用 mapTaskStatusToColor
配置了任务状态和任务显示颜色之间的映射关系,使用 mapTaskStatusToName
配置了任务状态和任务名称之间的映射关系。mapTaskStatusToColor
没有使用 Record
结合来枚举值 TaskStatus
来标注类型,所以在配置时 TaskStatus.DONE
没有配置对应的颜色也不会报错,当任务状态过多的时候你可能因为疏忽而没有设置某个任务状态对应的颜色,当你消费 mapTaskStatusToColor
这个对象时你的程序就可能会因为任务状态没有对应的颜色而出 BUG 。在 mapTaskStatusToName
使用了 Record<TaskStatus, string>
来限制对象的属性,因此在没有设置 TaskStatus.DONE
的任务状态名称时,TypeScript 会发现对象缺失了这一状态属性,你可以根据错误信息解决这个问题,从而避免了自己因为疏忽漏掉。
3.8.3 函数类型相关
ConstructorParameters<T>
ConstructorParameters
可以用来获取构造函数的构造参数,如示例所示:
type T0 = ConstructorParameters<ErrorConstructor>; // type T0 = [message?: string | undefined]
type T1 = ConstructorParameters<FunctionConstructor>; // type T1 = string[]
type T2 = ConstructorParameters<RegExpConstructor>; // type T2 = [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any>; // type T3 = unknown[]
type T4 = ConstructorParameters<Function>; // @errors: 2344
Parameters<T>
Parameters
的作用与 ConstructorParameters
类似,Parameters
可以用来获取函数的参数并返回序对,如示例所示:
declare function f1(arg: { a: number; b: string }): void;
type T0 = Parameters<() => string>;
//type T0 = []
type T1 = Parameters<(s: string) => void>;
//type T1 = [s: string]
type T2 = Parameters<<T>(arg: T) => T>;
// type T2 = [arg: unknown]
type T3 = Parameters<typeof f1>;
// type T3 = [arg: {
// a: number;
// b: string;
// }]
ReturnType<T>
ReturnType
的作用是用来获取函数的返回类型,如示例所示:
declare function f1(): { a: number; b: string };
type T0 = ReturnType<() => string>;
// type T0 = string
type T1 = ReturnType<(s: string) => void>;
// type T1 = void
type T2 = ReturnType<<T>() => T>;
// type T2 = unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
// type T3 = number[]
type T4 = ReturnType<typeof f1>;
// type T4 = {
// a: number;
// b: string;
// }
3.8.4 更多
更多工具类型可以参考工具类型 - TypeScript Documentation 。
四、参考资料
TypeScript 文档
TypeScript Documentation 是 TypeScript 的官方文档站点,官方文档对各个知识点的讲解很细致并且结合了示例,是学习 TypeScript 最好的方式之一。但是它的官方中文文档翻译的不全面,对英文不是很好的用户不太友好,中文文档推荐阅读 TypeScript Deep Dive 中文版。
TypeScript 一些在后续版本迭代中增加或者增强的特性,在版本更新日志和官方仓库中可以找到相应的介绍,在官方文档对应的特性介绍部分是没有的,例如 tuple-types - TypeScript: Documentation 中没有介绍命名元组成员(named tuple),而在TypeScript 4.2 版本更新日志中介绍了元组支持命名并提供了示例:
let d: [first: string, second?: string] = ['hello'];
d = ['hello', 'world'];
因此,在学习 TypeScript 的时候,不要局限于官方文档站,还可以通过以下途径学习:
这些站点中会提到 TypeScript 设计目标、使用、性能等方面的一些讨论,在官方文档站中是不一定有的。
TypeScript Playground
TypeScript Playground 是一个在线编辑器,用于探索 TypeScript 和 JavaScript。它是一个网站,支持设置在线环境的 tsconfig
配置以及使用 TypeScript 的版本,可以让您方便的编写、分享和学习 TypeScript。
Collection of TypeScript type challenges
Collection of TypeScript type challenges 是一个 TypeScript 在线类型编程题目集合,旨在帮助你更好地了解类型系统如何工作,学习编写你自己的工具类型。
如上图所示,你可以点击相应标签查看挑战的详细信息,然后在 TypeScript Playground 中开始挑战,也可以在支持 TypeScript 语言的 IDE 或文本编辑器中开始挑战。
Definitely Typed
Definitely Typed - high quality TypeScript type definitions 是一个项目,它提供了一个存储库,用于存储没有类型的 NPM 包的 TypeScript 类型定义。它是由社区维护的,包含了许多流行的 JavaScript 库的声明文件。
当你使用这些没有类型的 JavaScript 库时,你可以通过安装对应的 @types
包来获得类型支持。例如,如果你想要在 TypeScript 项目中使用 lodash 库,你可以安装 @types/lodash
包来获得类型支持。
npm install --save-dev @types/lodash
安装后,TypeScript 编译器会自动包含这些类型定义,你就可以在代码中获得类型提示和类型检查了。
React + TypeScript Cheat Sheets
React + TypeScript Cheat Sheets 是一个为有经验的 React 开发人员提供 TypeScript 入门指南的备忘单。它侧重于提供经过验证的最佳实践和可复制粘贴的示例,并在过程中解释一些基本的 TypeScript 类型使用和设置。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。