本篇文章来自团队小伙伴 @Vinchin🍊 的一次学习分享,希望跟大家分享与探讨。
求积硅步以致千里,勇于探享生活之美。
前言
现如今,对于前端来说 TypeScript 早已经不是什么新颖的技术了,世面上大多数开源项目也将它作为主要技术栈进行开发或提供支持。
但是对于大部分小伙伴来说,可能对 TypeScript 还停留在最基础的认知上,工作中的项目也很少有机会使用到,到最后只闻其名,不知其详。
所以我整理了一些知识点和对应的练习题,希望能通过这篇文章的分享让大家对 TypeScript 有更进一步的认识。
为什么使用 TypeScript
首先我们先观察两个简单的 JavaScript 代码片段
// 代码片段一
function numCompare(first, second) {
return first >= second ? first : second;
}
numCompare(12, 2); // => 12
numCompare('12', '2'); // => '2'
上面代码中定义了一个简单的数值比较函数,使用中不难发现当函数参数都为「数字型」字符串时,JS 会比较两个字符串 ASCII 码的大小,而不是比较数值的大小。因此会得到与我们期望不符的结果,导致后续的逻辑处理完全错误。
// 代码片段二
const toUpperCase = name => {name.toUpperCase()};
console.log('很多逻辑代码 1');
// => '很多逻辑代码 1'
console.log(toUpperCase(123));
// 报错:Uncaught TypeError: name.toUpperCase is not a function
console.log('很多逻辑代码 2 ');
// 不执行
第二个代码片段也很好理解,当代码运行时,往定义好的 toUpperCase
方法传入 number 类型参数,JS 直接就报错了,后面的代码块也因此中断不执行了。
类型造成的问题
相信大家很快就能发现,造成上面两处错误的原因,在于我们没有对函数的入参类型进行任何校验,导致我们只能在 JS 运行的时候才能发现这些错误。并且当错误产生的时候,影响到了后续代码的继续执行,整个应用也因为这小小的类型错误而出现逻辑错误甚至崩溃。
可能你会自信自己不会犯这种低级错误,或者添加一些容错代码来防止。但是当我们多人协同合作开发一个大型项目时呢?或者当我们使用第三方库时,又该如何避免因参数类型错误而导致的崩溃呢?
错误发现越早越好
在开发一个应用时,我们往往都希望 错误能越早发现越好 ,能在 写代码时 就发现的错误,就不要在 代码编译时 再发现,能在 代码编译时 就发现的错误,就不要在 代码运行时 再发现。
JavaScript 的痛点
最初,JavaScript 是专门为浏览器设计的脚本语言,只用于处理简单的用户交互、表单提交等。作为一门动态类型脚本语言,它没有类型系统,不需要编译即可在浏览器上运行,作者在设计时压根没有想到它会发展成现在这种规模。
但是随着 NodeJs 社区的繁荣发展和前端工程化的普及,前端项目代码量急剧膨胀,松散的代码,纷乱的类型给项目带来了更多的不稳定性和不可控性。
为了补全类型系统的缺失,微软在 JavaScript 的基础上设计出了 TypeScript 。
认识TypeScript
定义
TypeScript 是 JavaScript 类型的超集,它可以编译成普通、干净、完整的JavaScript代码。
我们可以简单理解成 types + JavaScript = TypeScript,types 代表 类型系统。
优势
- 支持所有的 JavaScript 特性,并且紧随 ECMAScript 标准,支持 ES6、ES7、ES8 等新的语法标准;
- 增加了类型的约束,还包括一些语法的拓展,比如枚举类型、元组类型等;
- 增加了代码的可读性和可维护性;
- 能在开发时就发现代码错误,无需在运行时才发现;
铺垫(shui)了这么多,开始进入正文,后面会在知识点中穿插一些练习题来加深对 TS 类型的理解。
类型注解
TypeScript 通过类型注解提供编译时的静态类型检查,可以在编译阶段就发现潜在的 bug,同时让编码过程中的提示也更加智能,简单来说可以理解成强类型语言中的类型声明。
// 代码块一
let str: string = 'hello world';
// 代码块二
function test(str: string): string {
return str;
}
代码块一声明了一个类型为 string
的变量 str
,告诉 ts 的类型检查系统 str
变量是一个字符串类型,在后续的使用和编译中,ts 会根据 类型注解 来进行报错和提示。
同理代码块二声明了一个 test
函数入参为字符串类型,且函数返回值类型也为 string
。
练习题一
为 numCompare
函数添加类型注解。
function numCompare(first, second) {
return first >= second ? first : second;
}
这道题目相信大家很快就能给出答案,这里就不过多解释,答案如下:
function numCompare(first: number, second: number): number {
return first >= second ? first : second;
}
类型推论
TypeScript 也很聪明,在没有明确指出类型的地方,它也能够帮我们推断变量类型,这叫做 类型推论,通常发生在初始化变量和成员,设置默认参数和决定函数返回值时。
let str = 'hello world';
基础类型
TypeScript 的类型大部分和 JavaScript 用法差不多,这里讲下不一样的地方
基础类型
- 字符串类型;
- 数字类型;
- 布尔类型;
- 对象类型;
数组类型
数组类型的声明方式有两种:
let arr: number[] = [1,2,3];
// or
let arr: Array<number> = [1,2,3];
元组类型
表示一个已知元素数量和类型的数组
let tuple: [string, number] = ['hello world', 0];
// success
tuple = [0, 'hello world'];
// error
tuple = ['hello world', 0, 1];
// error
Null & Undefined
默认情况下,null
和 undefined
是所有类型的子类型,可以将他们赋值给其他类型的变量。
Any
顶级类型,声明为 any
类型的变量可以赋予所有类型的值。
Unknown
顶级类型,它可以接收任何类型,但是无法赋值给其他类型(除 any
和 unknown
本身之外),因此在需要接收所有类型的场景下,优先考虑用 unknown
代替 any
。
Void
表示没有任何类型,通常当一个函数没有返回值时会使用到 void
function console(): void {
console.log('hello world');
}
Never
表示永远不存在的值,当一个函数永不返回时,它的返回值为 never
类型。
枚举类型
enum
类型是对 JavaScript 标准数据类型的一个补充。
数字枚举
默认情况下,成员的初始值会从 0 开始自动增长,也可以自定义初始值,支持反向映射,可以根据索引值反向获得枚举类型。
enum Color {Red, Green, Blue};
let c = Color.Green; // => 1
let c1 = Color[1]; // => Green
enum Color2 {Red = 1, Green, Blue};
let c = Color2.Green; // => 2
字符串枚举
不支持反向映射
enum Color {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
let c = Color.Red; // => Red
常量枚举
它是使用 const
关键字修饰的枚举,也称 const 枚举,不同于常规的枚举,它们在编译阶段会被删除,只保留使用到的枚举成员值。
const enmu Color {
Red,
Green,
Blue,
}
let c = [Color.Red, Color.Green, Color.Blue];
编译后的代码为:
var c = [0,1,2];
异构枚举
字符串和数字混合的枚举。
类型断言
有时候当你比 TypeScript 更了解某个变量值的详细信息,可以通过 **类型断言 **这种方式来覆盖它的判断, 类型断言有两种写法:
let value: any = 'hello world';
let len = (<string>value).length;
//or
let len = (value as string).length;
练习题二
解决 inputBgChange
函数报错,使函数能正常运行,在线调试
function inputBgChange(): void {
let oInput: HTMLInputElement;
if (document.querySelector('.oInput')) {
oInput = document.querySelector('#oInput');
oInput.style.background = 'red';
} else {
oInput = document.createElement('input');
oInput.id = 'oInput';
oInput.style.background = 'red';
document.body.appendChild(oInput);
}
}
document.querySelector('.oInput')
返回值为 HTMLInputElement
或者 null
类型,null
类型不能分配给 HTMLInputElement
,所以 TypeScript 会提示错误;最快的解决办法是将 oInput
声明成 any
类型,但是这种方式会使 TypeScript 类型系统完全失效,我们一般不建议这样使用。
由于 if (document.querySelector('.oInput'))
判断存在,我们可以很清楚的知道 document.querySelector('.oInput')
返回值类型不可能为 null
,所以可以使用类型断言告诉 TypeScript 类型系统,相信我,它的返回值就是 HTMLInputElement
类型。
答案:
function inputBgChange(): void {
let oInput: HTMLInputElement;
if (document.querySelector('.oInput')) {
oInput = document.querySelector('.oInput') as HTMLInputElement;
oInput.style.background = 'red';
} else {
oInput = document.createElement('input');
document.body.appendChild(oInput);
oInput.className = 'oInput';
oInput.style.background = 'red';
}
}
接口
接口的概念在 TypeScript 中至关重要,它主要是用来规范类型、描述对象或类的具体结构。
它就像是我们与 TypeScript 的类型系统约定好一个类型规范,后续的开发过程中,使用到该接口的变量必须按照约定好的类型规范进行定义和赋值,否则将无法通过类型系统的检查。
一般我们使用 interface
关键字来定义一个接口
interface Person {
age: number,
name: string,
}
let jack: Person;
jack = {
age: 17,
}
// error, 缺少 name 参数
jack = {
age: 17,
name: 'jack Ma',
sex: 'male'
}
// error, person 接口未定义 sex 参数
jack = {
age: 17,
name: 'jack Ma'
}
// error, 参数 age 必须为 number 类型
jack = {
age: 17,
name: 'jack Ma'
}
// success
上面的例子中,我们和 TypeScript 约定了一个名为 Person
的接口,接口定义了一个字符串类型的属性 name
,和数字类型的属性 age
,并且告诉 TypeScript 类型系统变量 jack
必须满足 Person
的接口定义,少一个属性、多一个属性、属性类型不对都不能通过。
可选属性
有时候我们可能只需要 jack
变量的 name
属性,我们可以在 age
属性后加上一个 ?
,告诉 TypeScript 变量 jack
的 age
属性不是必须的。
interface Person {
age?: number,
name: string,
}
let jack: Person;
jack = {
name: 'jack Ma',
}
// success
jack = {
name: 'jack Ma',
age: 17
}
// success
只读属性
当我们在属性名前加上 readonly
时,该属性值从定义赋值之后就不能再次修改。
interface Person {
readonly age: number,
name: string,
}
let jack: Person;
jack = {
name: 'jack Ma',
age: 17
}
jack.name = 'jack';
// success
jack.age = 18;
// error
任意属性
当不确定变量其他属性,又想绕过 TypeScript 的检测时,我们可以使用任意属性。
interface Person {
age: number,
name: string,
[propName: string]: any,
}
let jack: Person;
jack = {
age: 17,
name: 'jack Ma',
sex: 'male'
}
// success
我们通过 [propName: string]: any
约定了 Person
接口剩余属性名为 string
类型,值为 any
类型。
函数属性
除了描述带有属性的普通对象外,接口也可以描述函数类型。
interface CompareFunc {
(first:number, second: number): number;
}
let numCompare: CompareFunc = function(first: number, second: number) {
return first > second ? first : second;
}
// success
numCompare: CompareFunc = function(one: number, two: number) {
return one > two ? one : two;
}
// success
numCompare: CompareFunc = function(first, second) {
return first > second ? first : second;
}
// success
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。
练习题三
补充完整接口 User
的定义,并且为 users
提供更加准确的类型注解。
interface User {
// todo
}
// 将 unknow 替换成更准确的类型
let users: unknown = [
{
name: 'Jack Ma',
age: 17,
sex: 'male',
},
{
name: 'Tony Ma',
age: 18,
},
]
答案:
interface User {
name: string,
age: number,
sex?: string,
}
let users: User[] = [
{
name: 'Jack Ma',
age: 17,
sex: 'male',
},
{
name: 'Tony Ma',
age: 18,
},
]
高级类型
类型别名
类型别名会给一个类型起个新名字。
type StrType = string;
let str:StrType = 'hello world';
联合类型(|)
联合类型是将多个类型联合起来,让一个变量拥有两种或两种以上的类型。
type UnionType = string | number;
let str: UnionType = 'hello world';
let num: UnionType = 1;
任意类型与 never
联合都不受 never
影响
type Tnumber = number | never;
// number
type Tstring = string | never;
// string
交叉类型(&)
交叉类型是将多个类型合并为一个类型,可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
interface Person {
name: string,
age: number,
}
interface Animal {
walk: boolean,
}
let jack: Person & Animal = {
name: 'jack',
age: 17,
walk: true,
}
// success
let jack: Person & Animal = {
name: 'jack',
age: 17,
}
// error
let jack: Person & Animal = {
name: 'jack',
age: 17,
walk: true,
eat: true,
}
// error
任意类型与 never
交叉都得到 never
type Tnumber = number & never;
// never
type Tstring = string & never;
// never
操作符
typeof
typeof
操作符可以用来获取一个变量的声明类型。
const jack = {
name: 'jack Ma',
age: 18,
};
type Person = typeof jack;
// { name: string, age: number }
const hello:string = 'hello world';
type Hello = typeof hello;
// string
keyof
keyof
操作符用来获取一个类型所有的键值,与 Object.keys
类似,前者获取 interface
的键,后者获取对象的键。
interface Person {
name: 'jack Ma',
age: 17,
}
type T1 = keyof Person;
// 'name' | 'age'
通常 keyof
会配合着 typeof
使用:
interface Person {
name: string,
age: number
}
function getPersonValue(obj:Person, key: keyof typeof obj): string|number {
return obj[key];
}
上面的 key 参数的类型 keyof typeof obj
值为 'name' | 'age'
,这样可以对 key 值进行约束,避免使用时 key
值拼写错误导致的错误。
in
in
操作符通常用来实现枚举类型遍历
type Keys = 'name' | 'age';
type Person = {
[K in Keys]: any;
}
// { name: any, age: any }
泛型
泛型是强类型语言中比较重要的一个概念,它允许我们在编写代码的时候暂不指定类型,使用一些以后才指定的类型,在实例化时作为参数指明这些类型,合理的使用泛型可以提升代码的可复用性,让系统更加灵活。
了解泛型
假如我们有这样一个函数,传入一个由数字组成的数组,返回数组第一个值,那我们可以这样写。
function findFirst(arr: number[]): number {
return arr[0];
}
那当我们的数组是由字符串组成时,我们又需要修改成
function findFirst(arr: string[]): string {
return arr[0];
}
这样子写似乎没有真正解决我们的需求,如果后续还需要处理布尔值组成的数组,我们又需要写一个新的函数,这时候泛型就能很好解决我们的问题。
function findFirst<T> (arr: T[]): T {
return arr[0];
}
findFirst<number>([1,2,3]);
// => 1
findFirst<string>(['hello', 'world']);
// => 'hello'
我们在函数名后面用 <T>
表示泛型,其中 T
可以理解成一个类型传参,当我们使用这个函数的时候把类型当成参数传递给 TypeScript 的类型系统,告诉他之前声明 T
的真正类型,并进行类型检查。
实际上,我们使用的时候可以省略函数后面的泛型传参,因为 TypeScript 很聪明,它可以根据我们传入函数的参数类型来判断泛型真正的类型。
findFirst([1,2,3]);
// => 1
findFirst(['hello', 'world']);
// => 'hello'
T 可以使用其他名称代替,你可以将 <T>
换成 <P>
、 <K>
都是可以的,甚至我们还可以定义多个泛型变量。
function contact<T, K> (arr: T[], msg: K): string {
return `${arr[0]}-${msg}`
}
contact([1,2,3], 'hello')
// => '1-hello'
泛型约束 (extends)
有时候,泛型的功能过于强大和灵活,为了避免像 any
一样造成滥用,我们可以使用 extends
关键字对泛型进行约束作用。
interface Lengthwise {
length: number;
}
function getLength<T extends Lengthwise>(arg: T): number {
return arg.length;
}
现在 getLength
函数被 LengthWise
接口约束住了,我们传入的泛型参数必须包含 length
属性。
getLength(1)
//error, Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
当我们传入参数 1 时,T
变量被确定为 number
类型,但是由于我们限制了 T
类型必须包含 length
属性,所以这里没有通过类型检查,直接报错,正确用法应该是:
getLength([1,2,3])
// => 3
除此之外,extends
还常常配合三元运算符使用:
T extends U ? X : Y;
我们可以理解成当 T
包含的类型是 U
包含类型的子集,那么返回 X
类型,否则返回 Y
类型。
举个例子:
type Diff<T, U> = T extends U ? never : T;
type diff = Diff<string | boolean | number, string | number>;
// boolean
上面的判断中,当 T
是 U
的子集,则返回 never
类型,否则返回 T
,所以最后 diff
的类型是 never | boolean | never
,由于任意类型与 never
联合都不受 never
影响,所以最后 diff
类型为 boolean
。
练习题四
实现一个 If
工具泛型,接受一个条件 C,若 C 为 true
返回类型 T
类型,否则返回 F
类型。
type If<C, T, F> = any; // todo
type T1 = If<true, boolean, number>;
// T1 为 boolean 类型
type T2 = If<false, boolean, number>;
// T2 为 number 类型
答案:
type If<C extends boolean, T, F> = C extends true ? T : F;
泛型推断(infer)
infer
通常在条件判断中与 extends
一同出现,表示将要推断的类型变量,推断的类型变量可以在有条件类型的 true
分支中被引用。
这段话理解起来有点绕,我们通过下面的例子来理解它:
示例一:
type InferType<T> = T extends (infer R)[] ? R : T;
type T1 = InferType<string[]>;
// string
type T2 = InferType<number>;
// number
InferType
中定义了泛型变量 T
如果是 (infer R)[]
(R 为待推断的类型变量)的子集,则返回类型 R
,否则返回类型 T
;
当我们传入 string[]
时,InferType
的判断相当于string[] extends R[] ? R : string[]
,判断为 true
,R 被推断为 string
,所以 T1
的类型返回值为 string
;
当我们传入 number
时,InferType
的判断相当于 number extends R[] ? R : number
, 在这里 number
不符合 R[]
,所以 T2
的类型返回值为 number
。
示例二:
type InferObj<T> = T extends {type: infer R} ? R : false;
type T1 = InferObj<number>;
// false
type T2 = InferObj<{type: number}>
// number
同上,可以自行理解一下。
练习题五
实现 FuncReturnType
工具泛型,接受一个函数类型,并且推断出函数的返回值。
type FuncReturnType<T> = any; // todo
type fn = (name: string)=> boolean;
type T1 = FuncReturnType<fn>;
// T1 应为boolean类型
答案:
type FuncReturnType<T> = T extends (...args: any[]) => infer R ? R :never;
工具泛型
为了方便开发者更好的使用类型,TypeScript 内置了一些常用的工具泛型。
Partical
type Partial<T> = {
[key in keyof T]?: T[P]
}
Partical
可以将类型中的所有属性变成可选属性
interface Person {
name: string,
age: number,
}
type T1 = Partical<Person>
// 相当于
type T1 = {
name?: string,
age?: number,
}
Record<K, T>
type Record<K extends keyof any, T> = {
[key in K]: T
}
// keyof any => number | string | symbol
Record
通常用来申明一个对象
type T1 = Record<string, string>;
const jack: T1 = {
name: 'jack Ma',
age: '17',
}
Pick<T, K>
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
Pick
通常用来将 T
类型中存在的 K
键提取出来生成一个新的类型
interface Person {
name: string,
age: boolean,
sex: string,
}
type T1 = Pick<Person, 'name'|'age'>;
// 相当于
type T1 = {
name: string,
age: boolean,
}
Exclude<T, K>、Omit<T, K>、ReturnType …
TypeScript 还内置了许多工具泛型,这里不一一介绍了,感兴趣的同学可以自行去了解它的实现。
综合题
看到这里,相信你对 TypeScript 的整个类型系统和它的用法也有了一定的认识,让我们来挑战一下几道综合题加深一下理解。
练习题六
实现一个 post
方法,当我们请求地址 url
为 /user/add
时,请求参数 params
中必传字段为名字 name
和年龄 age
,性别 sex
为选填字段,其中除了 age
字段类型为 number
,其余字段类型都为 string
。
import axios from 'axios';
function post(url: any, params: any) {
return axios.post(url, params)
}
post('/user/del', {name: 'jack Ma', age: 17});
// 报错, url 传参错误
post('/user/add', {name: 'jack Ma'});
// 报错, 缺少请求参数 age 字段
post('/user/add', {name: 'jack Ma', age: 17});
// 请求成功
post('/user/add', {name: 'jack Ma', age: 17, sex: 'male'})
// 请求成功
答案:(实现的方案不唯一,能满足要求即可)
import axios from 'axios'
interface API {
'/user/add': {
name: string,
age: number,
sex?: string
}
}
function post<T extends keyof API> (url: T, params: API[T]) {
return axios.post(url, params)
}
post('/user/add',{name: 'jack Ma', age: 17})
练习题七
实现一个 Includes<T, K>
工具泛型,T
为一个数组类型,判断 K
是否存在于 T
中,若存在返回 true
,否则返回 false
type T1 = Includes<['name','age','sex'], 'name'>
// T1 的期望为 true
type T2 = Includes<['name','age','sex'], 'name'>
// T2 的期望为 false
答案:
type Includes<T extends any[], K> = K extends T[number] ? true : false;
这里由于 T extends any[]
,T 被约束成一个元素为 any
类型的数组,在 typescript 中,数组的类型是这样被声明的:
interface Array<T> {
[n: number]: T;
length: number;
toString(): string;
toLocaleString(): string;
pop(): T | undefined;
push(...items: T[]): number;
concat(...items: ConcatArray<T>[]): T[];
……
}
可以看到 [n: number]: T
这里约定了数组的下标类型为 number
,所以我们可以使用 T[number]
来表示数组 T
的元素。
练习题八
TypeScript 中有一个 ReadOnly<T>
工具泛型,它的功能是将 T
的所有属性变成只读属性
interface Person {
name: string
age: number
}
const jack: Readonly<Person> = {
name: 'jack Ma',
age: 17,
};
jack.name = 'jack';
// error, name 属性为只读
现在我们需要把它改造一下,实现一个 MyReadOnly<T, K>
,K
应为 T
的属性集,若指定 K
,则将 T
中对应的属性修改成只读属性,若不指定 K
,则将所有属性变为只读属性。
interface Person {
name: string,
age: number,
}
type MyReadOnly<T, K> = any;
const jack: MyReadOnly<Person, 'name'> = {
name: 'jack',
age: 17,
}
jack.name = 'jack';
// error
jcak.age = 18;
// success
答案:
第一步,我们先遍历一遍类型 K ,并将类型 T 中存在的 K 属性设置为 readonly
type MyReadOnly<T, K> = {
readonly [P in K]: T[P]
}
第二步,将 K 类型约束成 T 的子集
type MyReadOnly<T, K extends keyof T> = {
readonly [P in K]: T[P]
}
到这里,已经完成了只读属性的设置
第三步只需将剩余的属性拼接进去即可,这里我们可以使用 交叉类型&,相同属性名的情况下,若其中一个类型属性设置为只读,交叉最终返回的这个属性类型也会是只读。
type MyReadOnly<T, K extends keyof T> = {
readonly [P in K]: T[P]
} & T;
最后一步,还需要满足当 K 为空的时候,默认将 T 类型下所有属性设置为只读,我们可以给 K 传一个默认值 keyof T
最终答案为:
type MyReadOnly<T, K extends keyof T = keyof T> = {readonly [P in K]: T[P]} & T;
练习题九
实现一个 AppendArgX<Fn, X>
工具泛型,对于给定的函数类型 Fn
, 和任意的类型 X
,在 Fn
函数的传参末尾追加类型为 X
的参数
type Fn = (a: number, b: string) => number
type NewFn = AppendArgX<Fn, boolean>
// NewFn 期望是 (a: number, b: string, x: boolean) => number
答案:
type AppendArgX<Fn, X> = Fn extends (...args: infer P) => infer R ? (...args: [...P, X])=>R : never;
练习题十
实现一个 GetRequired<T>
工具泛型,将 T 中的所有必需属性提取出来
type T1 = GetRequired<{name: string, age: number, sex?: string}>
// T1 的期望是 {name: string, age: number}
答案:
这里我们可以分成两步来实现:
- 第一步先实现一个
RequiredKeys<T>
筛选出必需属性的键值; - 第二步再实现一个
GetRequired<T>
遍历返回必需属性;
type RequiredKeys<T> = keyof T extends infer K
? K extends keyof T
? T[K] extends Required<T>[K]
? K
: never
: never
: never;
// Required<T> 是 TypeScript 内置的工具泛型,可以将 T 所有属性设置为必需属性
type GetRequired<T> = {
[key in RequiredKeys<T>]: T[key]
};
关键在 T[K] extends Required<T>[K] ? K : never
这一步,它可以将所有的必选属性筛选出来
总结
本文主要简单分享了 TypeScript 的类型系统,并结合练习题的方式,希望能让读者对整个类型系统和一些常用的用法能有更加深刻认识,并且在日常工作中能更灵活使用它。
当然,除了文章里提及的知识和用法,TypeScript 还有很多有趣的知识等着大家去探索和发掘。
后续有机会还会分享一篇 TypeScript 实践文章,分享如何打造一个 TS 工具库,包含单元测试、打包方式、自动化部署等等,敬请期待。
参考文章
以上便是本次分享的全部内容,希望对你有所帮助 ^_^
喜欢的话别忘了动动手指,点赞、收藏、关注三连一波带走。
关于我们
我们是万拓科创前端团队,左手组件库,右手工具库,各种技术野蛮生长。
一个人跑得快,不如一群人跑得远。欢迎加入我们的小分队,牛年牛气轰轰往前冲。