十道题目带你走进 TypeScript 世界

4,844 阅读20分钟

本篇文章来自团队小伙伴 @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 用法差不多,这里讲下不一样的地方

基础类型

  1. 字符串类型;
  2. 数字类型;
  3. 布尔类型;
  4. 对象类型;

数组类型

数组类型的声明方式有两种:

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

默认情况下,nullundefined 是所有类型的子类型,可以将他们赋值给其他类型的变量。

Any

顶级类型,声明为 any 类型的变量可以赋予所有类型的值。

Unknown

顶级类型,它可以接收任何类型,但是无法赋值给其他类型(除 anyunknown 本身之外),因此在需要接收所有类型的场景下,优先考虑用 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 变量 jackage 属性不是必须的。

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

上面的判断中,当 TU 的子集,则返回 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}

答案:

这里我们可以分成两步来实现:

  1. 第一步先实现一个 RequiredKeys<T> 筛选出必需属性的键值;
  2. 第二步再实现一个 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 工具库,包含单元测试、打包方式、自动化部署等等,敬请期待。

参考文章

以上便是本次分享的全部内容,希望对你有所帮助 ^_^

喜欢的话别忘了动动手指,点赞、收藏、关注三连一波带走。


关于我们

我们是万拓科创前端团队,左手组件库,右手工具库,各种技术野蛮生长。

一个人跑得快,不如一群人跑得远。欢迎加入我们的小分队,牛年牛气轰轰往前冲。

VANTOP前端团队