# TypeScript类型系统实战课

https://github.com/linbudu599/QConPlus-Ts-Types 林不渡

# 结构化类型系统与标称类型系统

TS是结构化类型系统,它和标称类型系统有什么不同?

# 结构化类型系统的两个特点

如何判断一只鸟是鸭子?走路、游泳、捕食。

下面看一个比较反直觉的case。

class Cat {
  eat() {}
}

class Dog {
  eat() {}
}

function feedCat(cat: Cat) {} // 接受一个Cat类

feedCat(new Dog()); // 传送一个Dog类,类型检查通过

为什么这里传了一个Dog类,类型检查还是通过呢?因为TS是属于结构化类型系统,不是标称类型系统,上手难度会相对较低一些。

结构哈类型系统,只对比Dog类结构类型和Cat类结构类型是否一致,如果一致则检查通过。这是结构化类型系统第一个特点:基于类型结构的比较

而标称类型系统,会严谨的对比传入的参数是否为Cat类。

继续看看下面的case。

// Case1
class Cat {
  eat() {}
  jump() {}
}

class Dog {
  eat() {}
}

function feedCat(cat: Cat) {} // 接受一个Cat类

feedCat(new Dog()); // 报错,缺少jump方法

// Case2
class Cat {
  eat() {}
}

class Dog {
  eat() {}
 	jump() {}
}

function feedCat(cat: Cat) {} // 接受一个Cat类

feedCat(new Dog()); // 类型检查通过

当Dog类型不满足Cat时,此时报错符合预期。但是当Cat类为Dog类的子集时,此时类型检查又通过了,是为什么呢?

这是结构化类型系统第二个特点:自动的基类与派生类判断

当Dog类类型结构包含了Cat类所有的类型结构,这时会认为Dog是继承于Cat。代码如下:

// 代码原本书写
class Cat {
  eat() {}
}

class Dog {
  eat() {}
  jump() {}
}

function feedCat(cat: Cat) {} // 接受一个Cat类

feedCat(new Dog()); // 类型检查通过

// 结构化类型系统认为
class Cat {
  eat() {}
}

class Dog extends Cat { // 这里继承了Cat,因此认为满足
  jump() {}
}

function feedCat(cat: Cat) {} // 接受一个Cat类

feedCat(new Dog()); // 类型检查通过

因此,结构化类型系统的这两个特点会带来许多反直觉的问题。下面举个例子:

interface Foo {
  prop: any;
}

interface Bar {
  prop: any;
}

interface Baz {
  prop: any;
  uniqueProp: any;
}

declare let foo: Foo;
declare let bar: Bar;
declare let baz: Baz;

foo = bar; // 结构类型一样,满足
bar = foo; // 反之亦然,结构类型一样,满足

// a = b -> b < a
foo = baz; // baz完整包含foo类型结构,自动派生,满足
bar = baz; // baz完整包含bar类型结构,自动派生,满足

baz = foo; // 报错:反之,foo缺少uniqueProp属性,不满足
baz = bar; // 报错:反之,bar缺少uniqueProp属性,不满足

export {};

# 货币单位

通过货币单位结算的case来看看结构化类型系统的不足:

type USD = number;
type CNY = number;

const CNYCount: CNY = 200;
const USDCount: USD = 200;

function addCNY(source: CNY, input: CNY) { // 只接受CNY人民币
  return source + input;
}

addCNY(CNYCount, USDCount); // 检查通过

export {};

因为类型USD和类型CNY的底层类型都是number,虽然addCNY只接受CNY类型,但USD也能检查通过。

但这是我们不能接受的,因为人民币和美元存在汇率关系,这里需要报错才对。

TS这里可以尽可能的模拟标称类型系统来实现,代码如下:

export declare class TagProtector<T extends string> {
  protected __tag__: T;
}
// 原本的底层类型 + 类型的描述
// 区分 number 与 number
export type Nominal<T, U extends string> = T & TagProtector<U>; // 标称属性

export type CNY = Nominal<number, 'CNY'>;

export type USD = Nominal<number, 'USD'>;

const CNYCount = 100 as CNY;

const USDCount = 100 as USD;

function addCNY(source: CNY, input: CNY) {
  return (source + input) as CNY;
}

addCNY(CNYCount, CNYCount); // 检查通过

addCNY(CNYCount, USDCount); // 报错了!

通过以上case,我们能感知到结构类型系统和标称类型系统的区别了,也清楚TS在这一类类型出现反直觉问题的原因了。

# 从Top Type、Bottom Type到类型层级

# Top Type any

// Top Type
// 所有类型都是它的子类型
let anyVar: any = 'linbudu';

// a = b, b < a
anyVar = false;
anyVar = 'linbudu';
anyVar = {
  site: 'qconplus',
};

anyVar = () => {};

// Bottom Type
// 它是所有类型的子类型
// val = anyVar
// any 是一个身化万千的变量

// 1 和 0 没有交集,不存在一个类型同时满足 1 和 0
// IsAny
type Res = 1 & any;

const val1: string = anyVar;
const val2: number = anyVar;
const val3: () => {} = anyVar;
const val4: {} = anyVar;

// any 具有传染性
anyVar.foo.bar.baz();
anyVar[0][1][2].prop1;

export {};
  • any是Top Type,所有类型都是any的子类型
  • any也是Bottom Type,any是所有类型的子类型
  • any具有传染性,一个变量被标为any,此变量后续的所有操作都会被传染为any

# Top Type unknown

let unknownVar: unknown = 'linbudu';

unknownVar = false;
unknownVar = 'linbudu';
unknownVar = {
  site: 'qconplus',
};

unknownVar = () => {};

const val1: string = unknownVar; // Error
const val2: number = unknownVar; // Error
const val3: () => {} = unknownVar; // Error
const val4: {} = unknownVar; // Error

const val5: any = unknownVar;
const val6: unknown = unknownVar;

((unknownVar as { foo: () => {} }).foo() as { bar: any }).bar;

export {};
  • unknown相当于是类型安全的any

# Bottom Type never

// void null undefined
// != JavaScript 中的概念
// 作为类型存在,都是合法的,有具体意义的类型
// never 类型:虚无类型,表示不存在的类型

// 不会返回!
// return
function justThrow(): never {
  throw new Error();
}

// 类型控制流分析
function foo(input: number) {
  if (input > 1) {
    justThrow();
    // 等同于 return 语句后的代码,即 Dead Code
    const name = 'linbudu';
  }
}

declare let input: string | number | boolean | (() => {}) | never;

// 类型保护,instanceof、可辨识联合类型等
if (typeof input === 'string') {
	console.log(input); // 这里的推断一定是boolean奥,因为类型保护
  console.log('str!');
} else if (typeof input === 'number') {
  console.log('num!');
} else if (typeof input === 'boolean') {
  console.log('bool!');
} else {
  // never类型只能赋值给never类型变量
  // 通过never赋值给never,得到编辑时的报错。
  const _exhaustiveCheck: never = input; // 报错,因为input不一定是never、还可能是(() => {})
  // 运行时去抛出错误
  throw new Error(`Unknown input type: ${input}`);
}

export {};
  • never类型是真正的Bottom Type
    • void、null、undefined在TS中是合法的且有具体意义的类型
    • never类型,是一个虚无类型,表示不存在的类型
  • never只在两种情况下会显式使用这个类型:类型控制流分析类型保护
    • 通过typeof、instanceof、可辩识联合类型等进行类型保护!
    • 类型控制流分析,利用never类型赋值给never变量,得到一个编辑时的报错信息

# 字面量 继承 原始类型层级

// 字面量类型 - 对应的原始类型
// 数字、字符串、布尔值、对象字面量类型 -> 对应的原始类型
const name: 'linbudu' = 'linbudu1'; // 报错!必须完全符合linbudu,尽管都是字符串。
type Result1 = 'linbudu' extends string ? 1 : 2; // 1 字符串继承string
type Result2 = 1 extends number ? 1 : 2; // 1 数值继承number
type Result3 = true extends boolean ? 1 : 2; // 1 布尔继承boolean

// object:一切非原始类型的类型:数组、函数
// object 的字面量类型
type Result4 = { name: string } extends object ? 1 : 2; // 1,只有这个算是字面量类型
type Result5 = (() => void) extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1

export {};
  • TypeScript存在特殊的类型: 字面量类型(数字、字符串、布尔值、对象)。
  • object表示一切非原始类型的类型:数组、函数、对象等

# 字面量类型 继承 包含字面量的联合类型 继承 原始类型层级

// 字面量类型 - 包含字面量类型的联合类型 - 原始类型
// 联合类型
// 子集!
type Result1 = 1 | 2 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result2 = 'lin' extends 'lin' | 'bu' | 'du' ? 1 : 2; // 1
type Result3 = true extends true | false ? 1 : 2; // 1
type Result4 = string extends string | false | number ? 1 : 2; // 1

// 统一字面量联合类型到对应基础类型
// 字 字典
type Result5 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1
type Result6 = {} | (() => void) | [] extends object ? 1 : 2; // 1

export {};


// case  这里Result是2
type Result = 'linbudu' extends 'linbudu' | '599'  // 字面量类型是联合类型的子集
  ? 'linbudu' | '599' extends string // 统一字面量类型对应基础类型
    ? 2
    : 1
  : 0;

export {};
  • 联合类型是通过子集判断
  • 统一字面量联合类型对应基础类型

# 原始类型 继承 装箱类型 继承 小object 继承 大Object

// 装箱类型 String Number Boolean
type Result1 = string extends String ? 1 : 2; // 1

// 结构化类型带来的类型{},但是不合法,不符合类型层级定义
// {} -> 万物起源
type Result2 = String extends {} ? 1 : 2; // 1 但是我们不会这么使用
// object
type Result3 = {} extends object ? 1 : 2; // 1 

// 类型世界的基本规则
type Result4 = object extends Object ? 1 : 2; // 1
// object -> 非原始类型的类型
// 进行的判断定理不同
type Result5 = Object extends object ? 1 : 2; // 1

// 结构化
type Result6 = Object extends {} ? 1 : 2; // 1
// 字面量
type Result7 = {} extends Object ? 1 : 2; // 1

export {};

# any unknown类型层级

// Top Type 世界基本定理 类型层级的最顶端!
type Result1 = Object extends any ? 1 : 2; // 1
type Result2 = Object extends unknown ? 1 : 2; // 1

// 身化万千
// {} | unknown > {} extends Object + unknown extends Object
type Result3 = any extends Object ? 1 : 2; // 1 | 2  这里比较特殊,any不会判断,直接返回结果的联合类型
type Result4 = unknown extends Object ? 1 : 2; // 2

type Result5 = any extends 'linbudu' ? 1 : 2; // 1 | 2
type Result6 = any extends string ? 1 : 2; // 1 | 2
type Result7 = any extends {} ? 1 : 2; // 1 | 2
type Result8 = any extends never ? 1 : 2; // 1 | 2

type Result10 = any extends unknown ? 1 : 2; // 1
type Result11 = unknown extends any ? 1 : 2; // 1

export {};

# void undefined never类型层级

// Bottom Type
// 字面量类型 < 原始类型
type Result1 = never extends 'linbudu' ? 1 : 2; // 1

type Result2 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result3 = null extends 'linbudu' ? 1 : 2; // 2
type Result4 = void extends 'linbudu' ? 1 : 2; // 2

export {};
  • void undefined never是跟原始类型同级的类型,不是顶层或底层关系

# 类型层级总结

type TypeLevelChain = never extends 'linbudu' // never是最底层类型,因此满足
  ? 'linbudu' extends 'linbudu' | '599' // 字面量类型继承字面量联合类型,这里是子集,因此满足
    ? 'linbudu' | '599' extends string // 字面量联合类型继承原始类型,因此满足
      ? string extends String // 原始类型继承装箱类型,因此满足
        ? String extends Object // 装箱类型继承类型顶点Object,因此满足
          ? Object extends any // any是Top Type,因此满足
            ? any extends unknown // any和unknown 互相满足
              ? unknown extends any // any和unknown 互相满足
                ? 8
                : 7
              : 6
            : 5
          : 4
        : 3
      : 2
    : 1
  : 0;

// 结果为8
// 结构化类型系统 万物起源 {}

export {};
  • 判断a extends b是否满足,有两个要点
    • b层级需要高于a,例如'abc' extends string,原始类型层级高于字面量类型
    • 其次判断a是否为b的子集,例如100 extends string,string层级虽然高于字面量,但不是子集,因此不行。

# 分布式条件类型全知

# 泛型分配阻断

type Condition<T> = T extends 1 | 2 | 3 ? T : never;

// 1 | 2 | 3
// 1 extends 1 | 2 | 3 ? 1 : never + 2 extends 1 | 2 | 3 ? 2 : never + 3 extends 1 | 2 | 3 ? 3 : never
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;

// never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;

// 差异1:是否通过泛型参数传入

type Naked<T> = T extends boolean ? 'Y' : 'N';

// tuple
type Wrapped<T> = [T] extends [boolean] ? 'Y' : 'N';

type Res3 = Naked<number | boolean>; // "N" | "Y"

type Res4 = Wrapped<number | boolean>; // "N"

// 差异2:泛型参数是否裸露

// 共同点:联合类型

// 分布式条件类型的定义:当联合类型是通过泛型参数传入,会被分配推断

export {};

// **********
// 两种阻断方式
// **********

// (1 | 2)[] extends (1 | 2 | 3)[] ?
// 阻止裸露
type CompareUnion<T, U> = [T] extends [U] ? true : false;

// {} 万物起源,这里可以不用裸露T
type NoDistributiveConditionalType<T> = T & {};

type CompareRes1 = CompareUnion<1 | 2, 1 | 2 | 3>; // true
type CompareRes2 = CompareUnion<1 | 2, 1>; // false

export {};

TS冷门概念文章中,也有这个解释。

# 分布式:乘法分配率

// 差集
type Exclude<T, U extends T> = T extends U ? never : T; // T extends U 满足则过滤,否则输出

type Res1 = Exclude<1 | 2 | 3, 1 | 2>;

// 交集
type Extract<T, U> = T extends U ? T : never; // T extends U 满足则输出,否则显示

type Res2 = Extract<1 | 2 | 3, 1 | 2>;

// 并集!补集!
// [1,2,3] [1,2]

export {};

# 全等类型比较 isNever isUnknown isAny

// never any unknown
type IsNever<T> = [T] extends [never] ? true : false;

type IsNeverRes1 = IsNever<never>; // true
type IsNeverRes2 = IsNever<'linbudu'>; // false

// any 身化万千的 any
type Tmp1 = any extends string ? 1 : 2; // 1 | 2

// any > 直接拼接结果
type Tmp2<T> = T extends string ? 1 : 2;
type Tmp2Res = Tmp2<any>; // 1 | 2

// never 泛型参数 > never 
type Tmp3 = never extends string ? 1 : 2; // 1

type Tmp4<T> = T extends string ? 1 : 2;
type Tmp4Res = Tmp4<never>; // never

// 同时满足两边
type IsAny<T> = 0 extends 1 & T ? true : false;

// 一个个排除
type IsUnknown<T> = IsNever<T> extends false // 不是never
  ? T extends unknown 
    ? unknown extends T // 只有any和unknown,可以来回extends,确定是否为any或unknown
      ? IsAny<T> extends false // 排除any
        ? true
        : false
      : false
    : false
  : false;

export {};
  • any为泛型时,直接返回所有结果
  • never为泛型时,会拒绝判断

# 全等类型比较

type Conditional<A, B, Resolved, Rejected> = [A] extends [B]
  ? Resolved
  : Rejected;

// (1|2)[] extends (1|2)[]
// [A] extends [B] && [B] extends [A]
// 保留联合类型子类型的判断能力
// 完全比较两个联合类型相等?

type StrictConditional<A, B, Resolved, Rejected, Fallback = never> = [
  A
] extends [B]
  ? [B] extends [A]
    ? Resolved
    : Rejected
  : Fallback;

export {};

# 从类型系统到类型编程

# 了解内置类型系统的实现原理

学习路径:内置类型的原理了解、内置类型如何进阶、TypeChallenges Vue 底层工具类型实现

源码位置:typescript -> lib -> lib.es5.d.ts

# 访问性修饰工具

type Partial<T> = {
    [P in keyof T]?: T[P]; // ? = +? 添加可选符号
};

type Required<T> = {
    [P in keyof T]-?: T[P]; // -? 移除可选符号
};

type Readonly<T> = {
    readonly [P in keyof T]: T[P]; // readonly = +readonly,这里其实也有-readonly
};


// {foo:string, bar:string} K "foo"|"bar"
// 索引类型查询
// 索引类型声明
// {[K: string]: any;}
// map
// T["foo"] 索引类型访问
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// 工具类型的正向、反向类型实现
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Pick是基于已知键名进行结构裁剪
// 需求:未知键名,根据值类型进行裁剪,如何实现?学会扩展。
// 100个属性,50,50个字符串类型属性?数字+布尔值+函数

# 结构处理工具类型

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

# 集合工具类型

type Exclude<T, U> = T extends U ? never : T; // 并集
type Extract<T, U> = T extends U ? T : never; // 补集
// 交集、差集

# 模式匹配工具类型

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

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

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

# 模板字符串工具类型

type Uppercase<S extends string> = intrinsic;

type Lowercase<S extends string> = intrinsic;

type Capitalize<S extends string> = intrinsic;

# 复杂类型编程

复杂类型编程是由简单的处理工具类型组合而成,拆分、拆分、拆分!

// 类型编程的最常用思维:拆分
interface Struct {
  foo: string;
  bar: number;
  baz: boolean;
  handler: () => {};
}

// {"foo": "foo"}["foo"|"bar"|"baz"|"handler"]
// {}[] => obj["foo"] 取键值 
// 这里只能拿到键名{"foo": "foo"}
// :"foo" 不是值,而是"foo"字面量类型
type PickKeysByValueType<T, valueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never; // 拿到符合条件的键名
}[keyof T] // 这里的keyof T就是循环取键值 

// Pick拿到的键名和类型{"foo": string}
type PickByValueType<T, ValueType> = Pick<T, PickKeysByValueType<T, ValueType>>;

type Res1 = PickByValueType<Struct, string>; // {foo: string}

// {"foo": never, "bar": "bar", "baz": "baz", "handler": "handler"}
type OmitKeysByValueType<T, ValueType> = {
  	[Key in keyof T]-?: T[Key] extends ValueType ? never : Key;
}[keyof T]

封装实现逻辑相近的工具类型底层辅助实现,例如OmitKeysByValueTypePickKeysByValueType

更严谨的封装实现OmitKeysByValueTypePickKeysByValueType

/**
 * 实现基于值类型的 PickByValueType 与 OmitByValueType
 */

interface Struct {
  foo: string | number;
  bar: number;
  baz: boolean;
  handler: () => {};
}

export type PlainObjectType = Record<string, any>; // 判断是否为对象字面量

// 类型全等
type Conditional<Value, Condition, Resolved, Rejected> = Value extends Condition
  ? Resolved
  : Rejected;

// 开关 正向链路、反向链路
export type ValueTypeFilter<
  T extends object,
  ValueType,
  Positive extends boolean = true
> = {
  [Key in keyof T]-?: T[Key] extends ValueType
    ? Conditional<Positive, true, Key, never>
    : Conditional<Positive, true, never, Key>;
}[keyof T];

export type PickByValueType<T extends PlainObjectType, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType>
>;

export type OmitByValueType<T extends PlainObjectType, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType, false>
>;

// { foo: string }
type Res1 = PickByValueType<Struct, string>;

// { handler: () => {} }
type Res2 = PickByValueType<Struct, Function>;

/**
 * {
 *   bar: number;
 *   baz: boolean;
 *   handler: () => {};
 * }
 */
type Res3 = OmitByValueType<Struct, string>;

// 分布式条件类型导致的非预期情况
// (1|2)[] extends (1|2|3)[] // 这里不应该相等,但是会被判断为相等
// (1|2) (1|2)
// Equal

export {};

严格条件类型判断

import { expectType } from 'tsd';

export type PlainObjectType = Record<string, any>;

type StrictConditional<A, B, Resolved, Rejected, Fallback = never> = [
  A
] extends [B]
  ? [B] extends [A]
    ? Resolved
    : Rejected
  : Fallback;

export type StrictValueTypeFilter<
  T extends PlainObjectType,
  ValueType,
  Positive extends boolean
> = {
  [Key in keyof T]-?: StrictConditional<
    ValueType,
    T[Key],
    Positive extends true ? Key : never,
    Positive extends true ? never : Key,
    Positive extends true ? never : Key
  >;
}[keyof T];

export type StrictPickByValueType<T extends PlainObjectType, ValueType> = Pick<
  T,
  StrictValueTypeFilter<T, ValueType, true>
>;

export type StrictOmitByValueType<T extends PlainObjectType, ValueType> = Pick<
  T,
  StrictValueTypeFilter<T, ValueType, false>
>;

// {bar:1|2}
expectType<
  StrictPickByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2>
>({
  bar: 1,
});

expectType<
  StrictOmitByValueType<{ foo: 1; bar: 1 | 2; baz: 1 | 2 | 3 }, 1 | 2>
>({
  foo: 1,
  baz: 3,
});

// 完成了内置工具类型的进阶实现
// 高阶工具类型,Type Challenges
最近更新时间: 2023/7/24 20:46:22