Ts内置类型工具内部实现

182 阅读7分钟

前言

用TS好几年了,但一直没时间研究其内部实现。这篇文章用于探讨TS官网Utility Types章节提供的高阶类型工具方法的内部实现。

PS: 推荐两个github仓库类型体操习题实用类型库

操作符相关

在探讨具体内部实现时,我们首先需要关注几个操作符extends | infer | keyof | in keyof | typeof | Indexed Access Types 的作用,具体可以查看官网类型操作章节

extends

extends的使用场景有两种类型继承 | 条件类型

  • 条件类型: SomeType extends OtherType ? TrueType : FalseType (SomeType类型可以分配给OtherType时为true否则为false)
  • 如果给定类型 SomeType 扩展了另一个给定类型 OtherType,则 ConditionalType 为 TrueType,否则为 FalseType。
  • extends 意味着 SomeType 类型的任何值也是 OtherType子类型
// 1. 类型继承
interface A {
    name:string;
};
interface B extends A {
    age: number;
}
// 结果: B => {name:string; age: number;}

// 2. 条件类型
`SomeType extends OtherType ? TrueType : FalseType;`
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// 结果: type Example1: number。 因为DOg继承于Animal
type Example2 = RegExp extends Animal ? number : string;
// 结果: type Example2: string。 

infer

最开始类型推断infer表示在 extends 条件语句中待推断的类型变量。 在之后的版本更新内置了infer相关的映射类型提取函数的返回值类型 | 提取构造函数中的参数类型

1. 在 `extends` 条件语句中`推断的类型变量`
type IParam<T> = T extends (A: infer R ) => void ? R : any;
type A = (info: number) => void
type B = IParam<A>;
// 结果: B: number

2. 提取函数的返回值类型
type IReturnType<T> = T extends (...args: any) => infer R ? R : any;
type A = (info: number) => void
type B = IReturnType<A>;
// 结果 B:void。 推断出函数返回值

keyof

keyof用于对象类型并且生成数字字面量或者字符串的联合类型 (直白就是获取到对象的key组成一个新的联合类型)

type Point = { x: number; y: number, 1:string };
type P = keyof Point; 
// 结果:  P => 'x' | 'y' | 1 

in keyof

其通常用于将一个联合类型映射到对象属性中。

type Keys = 'name' | 'age'
type A  = {
    [key in keyof Keys]:number
}
// 结果: => A:{ name:number, age:number }

typeof

与JavaScript中的typeof类似,我们可以在类型上下文中引用变量或者属性的类型

let s = "hello";
let n: typeof s;
// 结果: let n:string

Indexed Access Types

我们可以使用索引访问类型来查找另一种类型的特定属性

// 当我们想获取Person对象下某个属性的类型
type Person = { age: number; name: string; alive: boolean };
type Age = Person['age'] // 结果: type Age:number;

// 结合keyof获取该对象所有的属性下的类型集合
type I2 = Person[keyof Person]; // type I2 = string | number | boolean

// 当然你可能只需要age和name的类型集合
type I3 = Person["age" | "name"]; // type I3 = string | number

内置类型工具

这里只分析一下高频的类型定义,针对上述的每一种操作符都会涉及到。其他有兴趣的同学可自行查看官网.

Partial

Partial<T>将泛型T下的所有属性都变成可选。

  • keyof T获取到的类型为 'name' | 'age',并且将'name' | 'age' 映射给泛型P
  • 通过索引访问类型获取到对应属性的类型,例如Info['name']的类型就为string
// 源码实现: 
type Partial<T> = {
    [P in keyof T]?: T[P];
};
    
// 应用
interface Info {
    name:string;
    age:number;
}
Type Info1 =  Partial<Info> 
Info1 => {name?: string; age?: number;}

Required

Required<T>正好与Partial<T>相反,其将泛型T下的所有属性都变成必选。

  • 大体他逻辑与上面一致。这里侧重讲一下-?的含义。-?意味着所有属性必须存在,又名删除可选性?
// 源码实现: 
type Required<T> = {
    [P in keyof T]-?: T[P];
};
    
// 应用   
interface Props {
    a?: number;
    b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 }; `报错,缺少b属性`

Readonly

Readonly<T>其将泛型T下的所有属性都变成只读状态,即不能修改其值。

  • 通过在属性前面添加readonly关键字,来声明该属性变为只读属性
// 源码实现: 
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";    `!!!报错啦,title为只读属性`

Record

Record<Keys, Type>,联合类型keys作为对象的key,Type类型作为对象属性的类型。在日常中我们声明一个未知对象通过Record<string, unknown>

  • 泛型K类型继承keyof any. 那么keyof any是什么呢? 其值为string | number | symbol
// 源码实现: 
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
    
// 应用  
interface CatInfo {
    age: number;
    breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
    miffy: { age: 10, breed: "Persian" },
};

Pick

Pick<T, K> 从T中选择一组属性,其键位于并集 K 中。(Pick的含义就是提取的意思)

  • 泛型K的值限定keyof T,也就是说Kkeyof T的子集,例如keyof T为'name' | 'age'那么K不可能超出这两个元素。
// 源码实现:   
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">; // description属性被排除了
const todo: TodoPreview = {
    title: "Clean room",
    completed: false,
};

Exclude

Exclude<T, U>从 T 中排除那些可分配给 U 的类型

  • T extends U 还记得前面提到条件语句吗,遍历T中的所有子类型,如果该子类型约束于U(存在于U、兼容于U)。存在则返回never类型,不存在则返回子类型

tips: Extract<T, U>与Exclude<T, U>正好相反,T extends U ? T : never满足条件返回子类型

// 源码实现:
type Exclude<T, U> = T extends U  ? never : T;

// 应用
type T0 = Exclude<"a" | "b" | "c", "a">;
// T0 => "b" | "c"

Omit

Omit<T, K> 构造一个具有T属性(类型 K 中的属性除外)的类型。结合Pick和Exclude实现,拆成如下两步

  • 首先Exclude<keyof T, K>排除掉K中属性,得到P
  • Pick<T, P>提取T符合P中的属性,构建成一个新的类型
// 源码实现:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 应用
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}
type TodoPreview = Omit<Todo, "description" | "completed">;
const todo: TodoPreview // TODO 为 {title: string}

Parameters

Parameters<T> 获取到函数中的参数类型

  • T extends (...args: any) => any表明泛型T必须是一个函数
  • 如果T满足(...args: any) => any条件则返回P类型,否则返回never。这个P是通过infer推断出来的类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

其他

使用?.可选操作符

const a = { }
如果直接获取a.b.d程序会报错,但是使用a?.d?.c程序会返回undefined

使用!.排除掉undefined|null标识这个值一定存在,有点断言as的味道

type IA = string
type IB = {
  name: string | undefined
};
const name1: IB = {
  name: '1'
};
const name2: IA = name1.name;
直接如上写会类型报错: Type string | undefined is not assignable string .....

const name2: IA = name1.name!;
 在name后追加!标识这个name不可能是undefined或者null

typescript中-?的意味着所有的属性都必须存在,又名删除可选属性

type T = {
    a: string
    b?: string
}
const sameAsT: { [K in keyof T]: string } = {
    a: 'asdf', // a is required
}

// Note a became optional
const canBeNotPresent: { [K in keyof T]?: string } = {
}

// Note b became required
const mustBePreset: { [K in keyof T]-?: string } = {
    a: 'asdf', 
    b: 'asdf'  // b became required 
}

const 与 Readonly的区别

它们实际上都做同样的事情,但一个用于变量,另一个用于属性

变量`const`不能被重新赋值,就像`readonly`属性一样。
本质上,当您定义属性时,您可以使用它`readonly`来防止重新分配。这实际上只是一个编译时检查。
当您定义`const`变量(并以更新版本的 JavaScript 为目标以保留`const`在输出中)时,也会在运行时进行检查。

interface 与 Type的区别

如下是官网提供的差异。几乎所有的interface功能都可以使用type来实现。最主要的区别是Type类型无法重新打开去添加新的属性,而接口类型总是扩展的 image.png

CONST断言

TypeScript 3.4 引入了一个名为 const 断言的字面值的新构造。

let name = 'AKclown' as const; // name的类型是'AKclown'

// 没有const断言
let name = 'x';   // name的类型是string

元组

规定数组对应元素的数据类型

// 两个元素,第一个元素为string类型、第二个元素为number
type info = [string,  number];

// 第一个元素为string,其余元素为number类型
type info = [string,  ...number[]];

tsc编译与babel的差异

图片引用 神说要有光的文章编译 ts 代码用 tsc 还是 babel? 640.png

640.png

参考链接