JavaScript技术

深入讲解Ts中高级类型工具

2022-06-03  本文已影响0人  没名字的某某人

写在最前:本文转自掘金

一、 前置内容

keyof 索引查询

对应任何类型Tkeyof T 的结果为该类型上所有共有属性key的联合:

interface Eg1{
  name: string;
  readonly age: number;
}
// T1的类型是 'name' | 'age'
type T1 = keyof Eg1

class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
}

// T2实则约束为 'age'
type T2 = keyof Eg2

T[K] 索引访问

interface Eg1{
  name: string;
  readonly age: number;
}

type V1 = Eg1['name']  // string
type V2 = Eg1['name' | 'age']  // string | number
type V2 = Eg1['name' | 'age222']  // any
type V3 = Eg1[keyof Eg1]  // string | number

T[keyof T] 的方式,可以获取到T所有key的类型组成的联合类型;注意:如果[]中的key有不存在T中的,则是any;因为ts也不知道该key最终是什么类型,且不会报错;

&交叉类型注意点

交叉类型取的多个类型的并集,如果相同key但类型不同,则该keynever

interface Eg1{
  name: string;
  age: number;
}
interface Eg2{
  color: string;
  age: string;
}
 type T = Eg1 & Eg2  // T的类型为{ name: string;age: never; color: string },注意,age因为两者接口内的类型不一致所有事never
// 可通过如下实例验证
const val: T = {
  name: ' ',
  color: ' ' ,
  age: (function a(){ throw Error() })(),
}

extends 关键字特性(重点)

特性一,用于接口,表示继承

interface T1{
  name: string;
}
interface T2{
  sex: number;
}

// T3 = {name: string; sex: number; age:number;}
interface T3 extends T1,T2{
  age: number,
}

注意,接口支持多重继承,语法为逗号隔开。如果是type实现继承,则可以使用交叉类型 type A = B& C & D

特性二,表示条件类型,可用于条件判断
表示条件判断,如果前面的条件满足,则返回问号后的第一个参数,否则第二个。类似于js 的三元运算。

A extends B,A为子类型,B为父类型 ,在接口中,属性约束宽泛为父类型,子类型应该继承父类型所有属性并加以更多属性约束。 在联合类型中,类型约束越宽泛为父类型,子类型应继承父类型基础上,缩减类型。

type A1 = 'x' extends 'x' ? 1: 2;  //  A1 = 1

type A2 = 'x' | 'y' extends 'x' ? 1: 2;  // A2 = 2

type P<T> = T extends 'x' ? 1: 2;
type A3 = P<'x' | 'y'>  // A3 = 1 | 2

为什么A2A3的值不一样:

总结,就是extends 前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断,然后最终结果组成新的联合类型。
如果不行被分发,可以通过简单的元组类型包裹一下:

type P<T> = [T] extends ['x'] ? 1 : 2;

type A4 = p<'x'|'y'>  // 2

类型兼容性

集合论中,如果一个集合的所有元素在集合B中都存在,则A是B的子集;
类型系统中,如果一个类型的属性更具体,则该类型是子类型。(因为属性更少则说明该类型约束更宽泛,是父类型)

因此,我们得到基本结论:子类型比父类型更加具体,父类型比子类型更宽泛。下面我们也将基于类型的可赋值性、协变、逆变、双向协变等进一步讲解。

可赋值性 子类型可赋值给父类型,反之不行

interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let a: Animal;
let b: Dog;

a = b // 子类可以赋值给更加宽泛的父类型
b = a // 反过来不行

可赋值性在联合类型中的特性

type A = 1 | 2 | 3
type B = 2 | 3
let a: A;
let b: B;

a = b // 可以赋值
b = a // 不可以赋值

是不是A的类型更多,A就是子类型呢?恰恰相反,A此处类型更多但表达的类型越宽泛,所有A是父类型,B是子类型。因此父类型不能给子类型赋值。

协变

interface Animal {
  name: string;
}
interface Dog extends Animal {
  break(): void;
}

let Eg1: Animal;
let Eg2: Dog;
// 兼容,可以赋值
Eg1 = Eg2

let Eg3: Array<Animal>
let Eg4: Array<Dog>
// 兼容,可以赋值
Eg3 = Eg4

通过Eg3Eg4来看,在AnimalDog在变成数组后,Array<Dog>依旧可以赋值给Array<Animal>,因此对于type MakeArray = Array<any>来说就是协变。
引用维基百科中的定义:

协变与逆变(Convariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

简单说,具有父子关系的多个类型,在通过某种构造器构造成的新的类型,如果还具有父子关系则是协变,而关系逆转了(子转父,父转子)就是逆变。
这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变,一种是父类型可以赋值给子类型,叫做逆变。

逆变

interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

type AnimalFn = (arg:Animal) => void
type DogFn = (arg: Dog) => void

let Eg1: AnimalFn;
let Eg2: DogFn;

Eg1 = Eg2; // 不可赋值
Eg2 = Eg1; //可赋值

理论上,Animal = Dog是类型安全的,那么AnimalFn = DogFn也应该类型安全猜对,为什么ts认为不安全呢?看下面例子:

let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog)=>{ arg.break() }

// 假设类型安全可以赋值
animal = dog;
// 那么animal 在调用时约束的参数缺少dog所需要的参数,此时会导致错误
// animal = (arg)=>{arg.break()}
animal({name: 'cat'});

从这个例子看到,如果dog函数赋值给animal函数,那么animal函数在调用时,约束的参数是Animal,但animal实际为dog的调用,传入参数无break()方法,此时就会出现错误。
因此,AnimalDog在进行type Fn<T> = (arg: T) => void 构造器构造后,父子关系就逆转了,此时称为逆变。

双向协变
ts在函数参数的比较中实际上默认采取的策略就是双向协变:只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。

这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息(典型的就是上述的逆变)。但是实际上,这极少会发生错误,并且能够时间很多JavaScript里常见模式:

// lib.dom.d.ts 中EventListener 的接口定义
interface EventListener{
  (evt: Event): void;
}
//  简化后的Event
interface Event {
  readonly target: EventTarget | null;
  preventDefault(): void;
}
// 简化合并后的MouseEvent
interface MouseEvent extends Event{
  readonly X: number;
  readonly Y: number;
}
// 简化后的window接口
interface window{
  // 简化后的addEventListener
  addEventListerner(type: string,listener: EventListener)
}

// 日常使用
window.addEventListener('click', (e: Event)=> {})
window.addEventListener('mouseover', (e:MouseEvent) => {})

可以看到windowlistener函数要去参数必须是Event,但是日常使用时更多时候传入的是Event子类型。但这里可以正常使用,正式其默认行为是双向协变的原因。可以通过tsconfig.js中修改strictFunctionType属性来严格控制协变和逆变。

重点infer关键词的功能暂时先不做详细说明,主要是用于extends的条件类型中让ts自己推断类型,具体的可以查阅官网。但关于infer的一些容易让人忽略的重要特性,必须提及一下:

infer推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void; } ? U : boolean;

type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;  // string
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;  // never

let Eg1: { a: (x: string | number) => void; b: (x: number | string) => void }  // 父类  
let Eg2: { a: (x: string) => void; b: (x: number) => void } // 子类
// 允许父类向子类赋值,为逆变,推到结果为交叉类型 never

infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;

type T3 = Foo<{ a: string; b: string }>;    // string
type T4 = Foo<{ a: number; b: string }>;    // string|number

let Eg3: { a: number | string; b: number | string }  // 父类
let Eg4: { a: number; b: string }  // 子类
// 允许子类向父类赋值,为协变,推到结果为联合类型 number| string

第二部分 ts内置类型工具原理解析

Partial 实现原理解析

Partial<T>T的所有类型变为可选的。

// 核心实现就是通过映射类型遍历T上所有的属性,
// 然后将每个属性设置为可选属性
···
type Partial<T> = {
  [P in keyof T]?: T[P];
}

扩展一下,将制定的key变成可选类型

/**
 *主要通过K extends keyof T 约束K必须为keyof T的子类
 *keyof T得到的是T的所有key组成的联合类型
 */
type PartialOptional<T, K extends keyof T>=P{
  [P in K]?:T[P];
}
/**
 *@example 
 *    type Eg1 = {key1?: string; key2?: number}
 */
type Eg1 = PartialOptional<{
  key1:string;
  key2:number;
  key3: '';
}, 'key1'|'key2'>

Readonly 原理解析

/*
 *主要通过映射遍历所有key,
 *然后给每个key增加一个readonly修饰符
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

/*
 * @example
 * type Eg1 = { readonly key1: string; readonly key2: number; }
 */
type Eg1 = Readonly<{
  key1: string;
  key2: number;
}>

pick

挑选一组属性并组成一个新的类型。

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

/* @example
 *  type Eg = {key1:string;key3:boolean;}
 *
 */
type Eg = Pick<{
  key1:string;
  key2: number;
  key3: boolean;
}, 'key1'|'key3'>

Record

构造一个typekey 为联合类型中的每个子类型,类型为T

/**
 * @example
 * type Eg = { a: {key1: string}; b: {key2: string} }
 */
type Eg = Record<'a' | 'b', {key1: string}>

Record具体实现

// k作为key,所有的类型仅为三种string|number|symbol ,使用keyof any表示
type Record<K extends keyof any, T> = {
  [P in K]: T
}

扩展:同态与非同态。划重点

// type Eg = {readonly a?: string}
type Eg = Pick<{readonly a?: string}, 'a'>

Eg的结果来看,Pick在拷贝属性时,连带拷贝了readonly?:修饰符。

Exclude原理解析

Exclude<T, U>提取存在于T,但不存在与U的类型组成的联合类型。

/*
 * 遍历T中的所有子类型,如果该子类型约束于U(存在于U,兼容于U)
 * 则返回never类型,否则返回该子类型
 */
  type Exclude<T, U> = T extends U ? never: T
/*
 * @example
 *   type Eg = 'key1'
 */
 type Eg = Exclude<'key1'| 'key2', 'key2'>

注意

// type Eg2 = string | number
type Eg2 = string | number | nerver

因此上述Eg其实就等于key1 | never,也就是key1

Extract

Extract<T, U> 提取联合类型T和联合类型U的所有交集。

/*
 * 遍历T中的所有子类型,如果该子类型约束于U(存在于U,兼容于U)
 * 则返回该子类型,否则返回never
 */
  type Extract<T, U> = T extends U ?  T: never
/*
 * @example
 *   type Eg = 'key2'
 */
 type Eg = Extract<'key1'| 'key2', 'key2'>

Omit原理分析

Omit<T, K>从类型T中剔除K中所有属性。

type Omit<T, K> = Pick<T ,Exclude<keyof T, K>>
/*
 * @example
 *  Eg = { key2: number; key3: boolean; }
 */
type Eg = Omit1<{key1:string;key2:number;key3:boolean},'key12'|'key1'|'key13'>

Parameters

Parameters 获取函数的参数类型,将每个参数类型放进一个元组中。

// 具体实现
type Parameters<T extends (...args: any) = >any>  =  T extends (...args: infer P) => any ? P :never;

// type Eg = [ arg1: string, arg2: number ]
type Eg = Parameters<(arg1: string, arg2: number) => void>;
// 普通元组
type Tuple1 = [ string, number? ];
let a: Tuple1 = [ 'aa', 11 ];
let a2: Tuple1 = [ 'aa' ];

// 具名元组
type Tuple2 = [ name: string, age?: number ];
let b: Tuple2 = [ 'aa', 11 ];
let b2: Tuple2 = [ 'aa' ];

扩展:infer 实现一个推导数组所有元素的类型

type FalttenArray< T extends Arrary<any> > = T extends Arrary<infer P> ? P : never;

// Eg1 = number | string;
type Eg1 = FalttenArray<[number, string]>

// Eg2 = 1 | 'as'
type Eg2 = FalttenArray<[1 | 'as']>

ReturnType 获取函数的返回值类型

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

ConstructorParameters

ConstructorParameters 可以获取类的构造函数的参数类型,存在一个元组中。

/*
 * 核心实现还是利用infer进行推导构造函数的参数类型
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

// @example type Eg = [name: string, sex?: number];
class People {
  constructor(public name: string, sex?: number) {}
}
type Eg = ConstructorParameters<typeof People>

那么,为什么要对T约束为abstract抽象类呢?看下面栗子:

class MyClass {}       // 定义一个普通类

abstract class MyAbstractClass {}    // 定义一个抽象类

let c1: typeof MyClass = MyClass  // 可以赋值
let c2: typeof MyClass = MyAbstractClass   // 报错,无法将抽象构造函数类型分配给非抽象构造函数类型

let c3: typeof MyAbstractClass = MyClass //可以赋值
let c4: typeof MyAbstractClass = MyAbstractClass  //可以赋值

由此可以看出,可以将抽象类(抽象构造函数)赋值给抽象类或者普通类,反之不行。

那么,为什么使用typeof 类作为类型呢,直接使用类作为类型又有什么区别呢?

// 定义一个类
class People{
  name: string;
  age: number;
  constructor() {}
}
let p1: People = new People   // 可以赋值
let p2: People = People // 不可以赋值 等号后面缺少name, age

let p3: typeof People = People  // 可以赋值
let p4: typeof People = new People()  // 不可以赋值,p4缺少prototype

简单的理解就是

最后,只需要对infer的使用换个位置,便可以获取构造函数返回值的类型:

type InstanceType<T extends abstract new (...args: any)=> any> = T extends abstract new (...args: any) => infer R ? R :any;
上一篇下一篇

猜你喜欢

热点阅读