skip to content
WNLee's Blog

Typescript学习总结

/ 32 min read

Typescript 是 Javascript 的超集而非一门真正的语言。它最终的运行是转化为 JavaScript。没有可以真实运行 Typescript 的环境...

一、前言

Typescript 是 Javascript 的超集而非一门真正的语言。它最终的运行是转化为 JavaScript。没有可以真实运行 Typescript 的环境。没有 TS 的日子 JS 依旧可以发展的很好。前端领域缺少对 TS 的硬性要求,缺乏考察标准。原始类型、数组、接口、枚举、函数、字面量、断言和 global.d.ts 覆盖了大部分使用场景。再深入的使用反而增加了使用成本和阅读成本。即使有心深入去了解 TS 如何应用,也存在比较高的门槛。因为它建立在“约定”之上,不具备底层逻辑性。

Typescript 带给前端开发者安全感同时提高效率。适当的类型限制,在开发阶段、构建阶段提高代码健壮性;使用 TS 在项目中定义变量类型,配合 IDE 进行异常提示、类型推导,提高开发效率。

二、基础概念

1. 模块系统

Typescript 的模块系统分为全局模块和文件模块(外部模块),它推荐使用 ES6 的语法规则 来导入/导出模块。最终编译为 JavaScript 进行运行,支持编译为多种规范的模块类型。事实上 Typescript 的模块系统应该是基于类型声明来讨论。Typescript 的模块系统可以归纳为以下三部分(民间归纳):

  • 基于 lib.d.ts/global.d.ts 的全局模块,同时支持 declare module xxx 的模块定义,作用于项目空间上下文;

  • 基于代码文件的文件模块(外部模块),支持 ES 语法的导入导出,作用于文件本身或者被导入文件;

  • 基于三方依赖的文件模块,特指 node_module 指定的类型定义文件,或者是代码文件同路径下的 xxx.d.ts 文件;

2. 使用接口

Typescript 使用结构化类型,结构化类型系统背后的思想是如果他们的成员类型是兼容的。接口旨在声明 JavaScript 中可能存在的任意结构。接口具备的一些特点:

  • 和内联注解结构相同时,两者相同;
  • 在同模块中名称相同的接口会合并;
  • 支持继承(extends)和实现(implements);

接口的一些使用实例:

  • 可调用接口
interface ReturnString {
  (): string;
}

declare const returnString: ReturnString;

const bar = returnString();
  • 可重载
interface Overloaded {
  (foo: number): number;
  (foo: string): string;
}

const overloaded: Overloaded = function(foo: any): any {
  if (typeof foo === 'number') {
    return foo * foo;
  } else if (typeof foo === 'string') {
    return `hello ${foo}`;
  }
}

const baz = overloaded(1);
const bar = overloaded('bar');
  • 可实例化
interface Crazy {
  new (): string;
}

declare const CrazyMan: Crazy;

const Jack = new CrazyMan();
  • 可实现(implements)
interface CrazyConstructor {
  new (name: string): CrazyInstance;
}
interface CrazyInstance {
  isMan(): Boolean;
}

const CrazyMan: CrazyConstructor = class implements CrazyInstance {
  constructor(name) {
    this.name = name;
  }
  
  isMan() {
    return true;
  }

  name: string;
}

const Jack = new CrazyMan('Jack');
3. 索引签名

TypeScript 的索引签名必须是 string 或者 number。symbols 也是有效的。所有成员都必须符合字符串的索引签名。讲索引签名是因为经常会碰到 Window 下的索引签名定义。

  • 所有成员都必须符合字符串的索引签名
interface Window {
  BridgeCall: () => void;
  [index: number]: string;
  [index: string]: string | (() => void);
  // [index: string]: any;
}
  • 使用一组有限的字符串字面量
type Behavior = 'EAT' | 'SLEEP' | 'BARK'
type Gog = {
  [index in Behavior]: () => void;
}

type ICRY = Gog['CRY'];
  • 索引签名中排除某些属性
// 报错
type FieldState = {
  value: string;
};

type FromState = {
  isValid: boolean; // Error: 不符合索引签名
  [filedName: string]: FieldState;
};

// 使用交叉类型 可以在索要签名中排除某些属性
type FieldState = {
  value: string;
};

type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };
4. ThisType

This 对 JavaScript 开发者来说并不陌生,编码过程中或多或少要使用到,所以对 This 的类型定义值得被拿出来记录。以下类型都是在配置 noImplicitThis: true 的情况

  1. 当 this 为方法入参时,定义 this 类型
const foo = {
  x: 1,
  m: function(this: {x: string}) {
    // this: {x: string}
  }
}

const foo = {
  x: 1,
  m: function(this) {
    // this: { x: number, m: (this...) => void }
  }
}
  1. 当类型有 ThisType 定义时,定义 this 类型
const foo: { x: number, m: () => void } & ThisType<{x: string}> = {
  x: 1,
  m: function() {
    // this: {x: string}
  }
};
  1. 当类型没有 ThisType 定义时,this 为对象字面的类型
type FooType = { x: number, m: () => void };
const foo: FooType = {
  x: 1,
  m: function() {
    // this: FooType
  }
};
  1. 重新定义 ThisType 类型的使用场景

当开启 noImplicitThis 的情况下,保持良好的开发习惯(不使用 this 作为入参),以上下文作为 this 类型比较准确;但是存在一些我们修改了 this 的情况就需要做特殊定义了。

// Compile with --noImplicitThis

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    }
  }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

三、类型分发(条件类型)

类型分发依赖 extends 关键词,extends 即可在继承时使用,也可以在类型分发使用。在类型分发的使用中它的意思是左边可分配给右边。

  1. 类型分发示例
interface Animal {
  live(): void;
}

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

type Condition<T> = T extends Animal ? true : false;
const c: Condition<Dog> = true;
  1. extends 的一些论证
// eg.1 对象字面量
type Human = { name: string; } 
type lookasHuman = { name: string; age: number; } 
type Bool = lookasHuman extends Human ? true : false; // true

// eg.2 联合类型
type A1 = 'x' extends 'x' ? string : number // string 

type A2 = 'x' | 'y' extends 'x' ? string : number // number 
type A3 = 'y' | 'x' extends 'x' ? string : number // number 
type A4 = 'y' extends 'x' | 'y' ? string : number // string 
type A5 = 'y' | 'x' extends 'x' | 'y' ? string : number // string 

// eg.3 分配条件类型
// 注意:当条件类型作用于泛型类型时,当给定联合类型时,它们就变成分布类型
// 举个例子:在条件类型 T extends U ? X : Y 中,当 T 是 A | B时,会拆分成 
// A extends U ? X : Y | B extends U ? X : Y
type P<T> = T extends 'x' ? string : number 
type B = P<'x' | 'y'> // string | number

// eg.4 防止条件判断中的分配
// 在条件判断类型的定义中,将泛型参数使用 [] 括起来,
// 即可阻断条件判断类型的分配,此时,传入参数T的类型将被当做一个整体,不再分配。
type P<T> = [T] extends ['x'] ? string : number; 
type A1 = P<'x' | 'y'> // number 
// never是所有类型的子类型 
type A2 = P<never> // string

3. extends 的另一个运用 - 泛型约束
// eg.1
type Color = 'green' | 'blue';
type Condition<T extends Color> = { [k in Color]: T }[T]

const c: Condition<'green'> = 'green';
const c2: Condition<'red'> = 'green'; // Error

四、类型推断

类型推断使 Typescript 变得强大,可以在代码的逻辑里推断出具体类型来。配合 IDE 的实时推断、代码提示,开发体验得到增强。当然在整个推导过程中,若中间环节被错误定义,便会导致推导结果达不到预期。这里建议项目中开启 noImplicitAny 避免一些隐藏问题。

  1. 定义变量 & 赋值

在定义变量过程中,会以值得内容作为类型

const foo = 123; // number
const bar = 'hello'; // string

type Adder = (a: number, b: number) => number;
let biz: Adder = (a, b) => a + b;

function iTakeAnAdder(adder: Adder) {
  return adder(1, 2);
}
iTakeAnAdder((a, b) => {
  return a + b;
});
  1. 函数返回

在函数里返回类型可以被 return 语句推断,没有返回则为 void 类型

function add(a: number, b: number) {
  return a + b;
}

const sum = add(1, 2); // number
  1. 结构化

这些简单的规则也适用于结构化的存在(对象字面量)

const foo = {
  a: 123,
  b: 456,
};

foo.a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型

数组

const bar = [1, 2, 3];
bar[0] = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
  1. 解构

对象字面量:

const foo = {
  a: 123,
  b: 456,
};

let { a } = foo;
a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型

数组:

const bar = [1, 2];
let [a, b] = bar;

a = 'hello';

函数参数:

type Adder = (n: {a: number; b: number}) => number;
function iTakeAnAdder(adder: Adder) {
  return adder({a: 1, b: 2});
}

iTakeAnAdder(({a, b}) => {
  a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
  return a + b;
});
  1. inter
  • 概念:

Inter 的出现使类型推断能力提升,inter 可以理解为一个中间量,用于指代某个未知变量。或者可以看出是占位。结合 extends 使用可以用于推导未知类型。使用场景:

type Parameters<T> = T extends (...args: infer R) => any ? R : any;
type Adder = (a: number, b: number) => number;
type T0 = Parameters<Adder>; // [number, number]
  • 内置类型:

    • ReturnType
    type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
    • ConstructorParameters
    type ConstructorParameters<T extends new (...args: any[]) => any> = 
      T extends new (...args: infer P) => any
        ? P
        : never;
    
    type InstanceType<T extends new (...args: any[]) => any> =
      T extends new (...args: any[]) => infer P
        ? P
        : any;
  • tuple 转 union

type TTuple = [string, number];
type TArray = Array<string | number>;

type ElementOf<T> = T extends Array<infer E> ? E : never;
type ToUnion = ElementOf<TTuple>;
  • Union 转 intersection
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) 
    ? I 
    : never;
    
type T1 = { name: string }
type T2 = { age: number }

type Reg = UnionToIntersection<T1 | T2> // T1 & T2
  1. 类型保护是类型推断的结果

五、类型兼容

类型兼容性用于确定一个类型是否能赋值给其他类型。 如 string 类型与 number 类型不兼容:

let str: string = 'Hello';
let num: number = 123;

str = num; // Error: 'number' 不能赋值给 'string'
num = str; // Error: 'string' 不能赋值给 'number'
  1. 安全性

类型兼容不一定安全,比如,小狗属于动物。说明小狗和动物是兼容的,但不是所有的动物都会叫,这里就会导致一定安全风险。尽可能的缩小类型范围,减少安全性风险

let foo: any = 123;
foo = 'Hello';

foo.toPrecision(3);
  1. 结构化

Typescript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了:

interface Point {
  x: number;
  y: number;
}

class Point2D {
  // public 是 Point2D实例具有 x, y 属性
  constructor(public x: number, public y: number) {}
}

let  p: Point;
p = new Point2D(1, 2);

这允许你动态创建对象,并且它如果能被推断,该对象仍然具有安全性

interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

const point2D: Point2D = { x: 0, y: 0 };
const point3D: Point3D = { x: 0, y: 0, z: 0};
function iTakePoint2D(point: Ponit2D) {
  // do something
}

iTakePoint2D(point2D);
iTakePoint2D(point3D);
iTakePoint2D({ x: 0 }); // Error: 没有 'y'
  1. 变体

对类型兼容性来说,变体是一个利于理解和重要的概念。 对一个简单类型 Base 和 Child 来说,如果 Child 是 Base 的子类,Child 的实例能被赋值给 Base 类型的变量。 在由 Base 和 Child 组合的复杂类型的类型兼容性中,它取决于相同场景下的 Base 与 Child 的变体:

  • 协变(Covariant):只在同一个方向,示例:Base -> Child,SuperType -> SuperType
  • 逆变(Contravariant):只在相反的方向,示例:Base -> Child,SuperType -> SuperType
  • 双向协变(Bivariant):包括同一个方向和不同方向,示例:Base -> Child,SuperType <-> SuperType
  • 不变(Invariant):如果类型不完全相同,则它们是不兼容的,示例:Base -> Child,SuperType <!=> SuperType
  1. 函数
  • 返回类型

协变:返回类型必须包含足够的数据。

  • 参数数量

更少的参数数量是好的(如:函数能够选择性的忽略一些多余的参数),但是你得保证有足够的参数被使用了。换言之,函数参数随数量减少协变:

// eg.1
type T1 = (a: number, b: number) => number;
type T2 = (a: number) => number;

type Reg0 = T2 extends T1 ? true : false // true;

let func1: T1 = function (a, b) { return a + b };
let func2: T2 = function (a) { return Math.floor(a) };
func1 = func2;
func2 = func1; // Error: 'func1' 不能赋值给 'func2'

// eg.2
const iTakeSomethingAndPassItAnErr = (x: (err: Error, data: any)) => {
  // do something
};

iTakeSomethingAndPassItAnErr(() => null);
iTakeSomethingAndPassItAnErr(err => null);
iTakeSomethingAndPassItAnErr((err, data) => null);

iTakeSomethingAndPassItAnErr((err, data, more) => null); // Error

// eg.3
type P1 = (n: { a: number, b: number }) => void;
type P2 = (n: { a: number }) => void;

type Result = P2 extends P1 ? true : false; // true
  • 可选的和 rest 参数

可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的:

1. 在 strictNullChecks 为 false 时,可以理解为双向协变
2. 在 strictNullChecks 为 true 时,确定参数和 rest 参数为双向协变,可选参数为不变性;
// 可选的(上例子中的 bar)与不可选的(上例子中的 foo)仅在选项为 strictNullChecks 为 false 时兼容。
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};

foo = bar = bas;
bas = bar = foo;
  • 函数参数类型

    • 在 strictFunctionTypes 为 false 的情况下是双向协变的,旨在支持常见的事件处理方案
    • 在 strictFunctionTypes 为 true 的情况下是逆变的
// 事件等级
interface Event {
  timestamp: number;
}
interface MouseEvent extends Event {
  x: number;
  y: number;
}
interface KeyEvent extends Event {
  keyCode: number;
}

// 简单的事件监听
enum EventType {
  Mouse,
  Keyboard
}
function addEventListener(eventType: EventType, handler: (n: Event) => void) {
  // ...
}

// 不安全,但是有用,常见。函数参数的比较是双向协变。
addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// 在安全情景下的一种不好方案
addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// 仍然不允许明确的错误,对完全不兼容的类型会强制检查
addEventListener(EventType.Mouse, (e: number) => console.log(e));
  1. 枚举
  • 枚举与数字类型相互兼容
  • 来自于不同枚举的枚举变量,被认为是不兼容的
  • 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查:
class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(meters: number) {}
}

let a: Animal = new Animal('', 1);
let s: Size = new Size(1);

a = s; // OK
s = a; // Ok
  • 私有的和受保护的成员必须来自于相同的类:
class Animal {
  protected feet: number;
}
class Cat extends Animal {}

let animal: Animal = new Animal();
let cat: Cat = new Cat();
animal = cat; // ok
cat = animal; // ok

class Dog extends Animal {
  protected canWang: boolean;
}
let dog: Dog = new Dog();
animal = dog; // OK !!!
dog = animal; // Error

class Size {
  protected feet: number;
}
let size: Size = new Size();
animal = size; // ERROR
size = animal; // ERROR
  1. 泛型
  • Typescript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才影响兼容性。如下例子中,T 对兼容性没有影响:
interface Empty<T> {}

let x: Empty<number>;
let y: Empty<string>;

x = y; // ok
  • 当 T 被成员使用时,它将在实例化泛型后影响兼容性:
interface Empty<T> {
  data: T;
}

let x: Empty<number>;
let y: Empty<string>;

x = y; // Error
  • 如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:
let identity = function<T>(x: T): T {
  // ...
};

let reverse = function<U>(y: U): U {
  // ...
};

identity = reverse; // ok
  • 类中的泛型兼容性与前文所提及一致:
class List<T> {
  add(val: T) {}
}

class Animal {
  name: string;
}
class Cat extends Animal {
  meow() {
    // ...
  }
}

const animals = new List<Animal>();
animals.add(new Animal()); //ok
animals.add(new Cat()); // ok

const cats = new List<Cat>();
cats.add(new Animal()); // Error
cats.add(new Cat()); // ok

六、类型保护

类型声明后,包含接口定义、赋值、参数等对变量起到类型约束作用。类型保护在这个基础进一步缩小范围,使类型定义在一个范围内更准确,更安全。

  1. Typeof

当使用 typeof 明确某个变量类型之后,在该分支下该变量的类型得到保护,类型明确。

function doSome(x: number | string) {
  if (typeof x === 'string') {
    console.log(x.subtr(1)); // Error
    console.log(x.substr(1));
  }
  
  x.substr(1); // Error
}
  1. Instanceof

与 typeof 类似,当使用 instanceof 明确某个变量的原型对象时,在该分支下该变量的类型得到保护,类型明确。

class Foo {
  foo = 123;
  common = '123';
}
class Bar {
  bar = 123;
  common = '123';
}

function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  }
  if (arg instanceof Bar) {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

doStuff(new Foo());
doStuff(new Bar());
  1. In

In 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用

interface A {
  x: number;
}
interface B {
  y: string;
}

function dStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}
  1. 字面量类型保护

可以通过明确对象字面量的属性类型来区分对象字面量,在该分支下对象字面量类型明确。

type Foo = {
  kind: 'foo';
  foo: number;
};
type Bar = {
  kind: 'bar';
  bar: number;
};

function doStuff(arg: Foo | Bar) {
  if (arg.kind === 'foo') {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}
  1. 使用定义的类型保护

使用自定义的类型保护函数

interface Foo {
  foo: number;
  common: string;
}
interface Bar {
  bar: number;
  common: string;
}

function isFoo(arg: Foo | Bar): arg is Foo {
  return (arg as Foo).foo !== undefined;
}

七、声明合并

Declaration TypeNamespaceTypeValue
Namespacexx
Classxx
Enumxx
Interfacex
Type Aliasx
Functionx
Variablex
  1. 函数合并

同个命名空间内的出现相同的函数声明被看做是函数重载声明

function func(a: string): string;
function func(a: number): number;
function func(a: any): any {
  console.log(a);
};
  1. 接口合并

同个命名空间内相同的接口声明会触发合并,接口里相同的方法属性声明会触发重载,非方法的属性声明重复弱类型不同会报错

interface A {
  a: string;
}

interface A {
  b: string;
}

let a1: A = { a: '1' }; // Error 
  1. 类的合并

类不能与其它类或变量合并

  1. 命名空间合并

同名命名空间会合并其成员,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口;对于命名空间里值得合并,如果当前已存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。

namespace Animals {
  export class Zebra {}
}
namespace Animals {
  export interface Legged { num: number }
  export class Dog{  }
}

// 等同于
namespace Animals {
  export interface Legged { num: number; }
  export class Zebra {}
  export class Dog {} 
}

非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其他命名空间合并进来的成员无法访问非导出成员。

namespace Animal {
  let haveMuscles = true;
  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}

namespace Animal {
  export function doAnimalsHaveMuscles() {
    return haveMuscles; // Error
  }
}
  1. 命名空间与类、函数、枚举类型合并
  • 合并命名空间和类

合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性

class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel { }
}
  • 合并命名空间和函数

创建一个函数稍后扩展它增加一些属性也是很常见的。 TypeScript使用声明合并来达到这个目的并保证类型安全。

function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
  export let suffix = "";
  export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));
  • 合并命名空间和枚举

命名空间可以用来扩展枚举类型

enum Color {
  red = 1,
  green = 2,
  blue = 4
}

namespace Color {
  export function mixColor(colorName: string) {
    if (colorName == "yellow") {
      return Color.red + Color.green;
    }
    else if (colorName == "white") {
      return Color.red + Color.green + Color.blue;
    }
    else if (colorName == "magenta") {
      return Color.red + Color.blue;
    }
    else if (colorName == "cyan") {
      return Color.green + Color.blue;
    }
  }
}

八、实践总结

  1. Event Emitter
interface Listener<T> {
  (event: T): any;
}
interface Disposable {
  dispose(): any;
}

class TypedEvent<T> {
  private listeners: Listener<T>[] = [];
  private listenersOncer: Listener<T>[] = [];
  
  public on = (event: Listener<T>): Disposable => {
    this.listeners.push(event);
    return {
      dispose: () => this.off(event),
    };
  };
  public once = (event: Listener<T>): void => {
    this.listenersOncer.push(event);
  };
  
  public off = (event: Listener<T>): void => {
    const callbackIndex = this.listeners.indexOf(event);
    if (callbackIndex > -1) {
      this.listeners.splice(callbackIndex, 1);
    }
  };
  public emit = (event: T) => {
    this.listeners.forEach(listener => listener(event));
    this.listenersOncer.forEach(listener => listener(event));
    
    this.listenersOncer = [];
  };
  
  public pipe = (te: TypedEvent<T>): Disposable => {
    return this.on(e => te.emit(e));
  };
}

const onFoo = new TypedEvent<Foo>();
const onBar = new TypedEvent<Bar>();

onFoo.emit(foo);
onBar.emit(bar);

onFoo.on(foo => console.log(foo));
onBar.on(bar => console.log(bar));
  1. 定义请求库
  • Fetch 的类型定义
interface IUser { 
  nick: string;
  age: number;
}

function getJson<T>(config: { url: string; headers?: { [key: string]: string } }) {
  const fetchConfig = {
    method: 'GET',
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(config.headers || {})
  };
  return fetch(config.url, fetchConfig).then<T>(response => response.json());
}

function queryUser() {
  return getJson<IUser>({
    url: './getUser',
  });
}
  • 有意思的请求库类型定义
type ResponseType
  = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';

type RequestResponse<T = any> = {
  data: T;
  response: Response;
}

interface ResponseError<D = any> extends Error {
  name: string;
  data: D;
  response: Response;
  request: {
    url: string;
    options: RequestOptionsInit;
  };
  type: string;
}

interface Context {
  req: {
    url: string;
    options: RequestOptionsInit;
  },
  res: any,
}

interface RequestOptionsInit extends RequestInit {
  charset?: 'utf8' | 'gbk';
  requestType?: 'json' | 'form';
  data?: any;
  params?: object | URLSearchParams;
  paramsSerializer?: (params: object) => string;
  responseType?: ResponseType;
  useCache?: boolean;
  ttl?: number;
  timeout?: number;
  timeoutMessage?: string;
  errorHanlder?: (error: ResponseError) => void;
  prefix?: string;
  suffix?: string;
  throwErrIfParseFail?: boolean;
  parseResponse?: boolean;
  cancelToken?: any;
  getResponse?: boolean;
  validateCache?: (url: string, options: RequestOptionsInit) => boolean;
  __umiRequesstCoreType__?: string;
  [key: string]: any;
}
interface RequestOptionsWithResponse extends RequestOptionsInit {
  getResponse: false;
}
interface RequestOptionsWithoutResponse extends RequestOptionsInit {
  getResponse: true;
}

type RequestInterceptor = (
  url: string,
  options: RequestOptionsInit
) => {
  url?: string;
  options?: RequestOptionsInit;
};

type ResponseInterceptor = (
  response: Response,
  options: RequestOptionsInit
) => Response | Promise<Response>;

type OnionMiddleware = (ctx: Context, next: () =r> void) => void;
type OnionOptions = { global?: boolean; core?: boolean; defaultInstance?: boolean; };

interface RequestMethod<R = false> {
  <T = any>(url: string, options: RequestOptionsWithResponse): Promise<RequestResponse<T>>;
  <T = any>(url: string, options: RequestOptionsWithoutResponse): Promise<T>;
  <T = any>(url: string, options?: RequestOptionsInit): R extends true ? Promise<RequestResponse<T>> : Promise<T>;

  interceptors: {
    request: {
      use: (handler: RequestInterceptor, options?: OnionOptions) => void;
    };
    response: {
      use: (handler: ResponseInterceptor, options?: OnionOptions) => void;
    };
  };
  use: (handler: OnionMiddleware, options?: OnionOptions) => void;
}

declare var request: RequestMethod;

export default request;
  1. 单例模式
class Singleton {
  private static instance: Singleton;
  private constructor() {}
  
  public static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    
    return Singleton.instance;
  }
  
  someMethod() {}
}

let someThing = new Singleton();
let instance = Singleton.getInstance();
  1. 其他一些使用
  • Never 使用

Never 表示不存在的值的类型,有很多用户

  • 原始类型的交叉类型
type N = string & number;

  - 提示需要处理新增类型
type foo = { type: 'foo' }
type baz = { type: 'baz' }
type biz = { type: 'biz' }

type TType = foo | baz | biz;

function func(type: TType) {
  switch(type.type) {
    case 'foo':
      break;
    case 'baz':
      break;
    default:
      const a: never = type;
      console.log(`${a} undefined`);
      break;
  }
}
  • 作为条件语句的值
type foo = { type: 'foo' }
type baz = { type: 'baz' }

function func<T>(type: any) {
  type TType = T extends foo 
    ? foo
    : never;
  // dosomething
}

// 等同于
function func<T extends foo>(type: any) {
  type TType = T
  // dosomething;
}
  • 定义 addEventListener

    • eg.1 不完美的实现方式
    interface Event {
      timestamp: number;
    }
    interface MouseEvent extends Event {
      x: number;
      y: number;
    }
    interface KeyEvent extends Event {
      keyCode: number;
    }
    
    enum EventType {
      Mouse = 'Mouse',
      Keyboard = 'Keyboard',
    }
    
    interface Type2Event {
      [EventType.Mouse]: MouseEvent,
      [EventType.Keyboard]: KeyEvent,
    };
    
    type EventParamType<T extends EventType> = Type2Event[T];
    
    function addEventListener(eventType: EventType.Mouse, handler: (n: MouseEvent) => void): void;
    function addEventListener(eventType: EventType.Keyboard, handler: (n: KeyEvent) => void): void;
    function addEventListener(eventType: EventType, handler: any) {
      switch(eventType) {
        case EventType.Mouse: 
          const ev: EventParamType<typeof eventType> = { x: 0, y: 1, timestamp: 123};
          handler(ev);
          break;
        case EventType.Keyboard:
          handler();
          break;
        default:
          const a: never = eventType;
          console.log(`${a} undefined`);
          break;
      }
    }
    
    addEventListener(EventType.Mouse, function(e) {
      console.log(e.x, e.y)
    });
    addEventListener(EventType.Keyboard, function(e) {
      console.log(e.keyCode);
    });
    • eg2
    interface Event {
      timestamp: number;
    }
    interface MouseEvent extends Event {
      x: number;
      y: number;
    }
    interface KeyEvent extends Event {
      keyCode: number;
    }
    
    interface EventMap {
      'Mouse': MouseEvent,
      'Keyboard': KeyEvent,
    }
    
    interface AddEventListenerType {
      <T extends keyof EventMap>(eventType: T, handler: (e: EventMap[T]) => void): void;
      (eventType: string, handler: (e: Event) => void): void;
    }
    
    type FactoryType = <T>(e: T extends keyof EventMap ? EventMap[T] : never) => void
    const createFactory = (h: (e: Event) => void): FactoryType => {
      return (e) => h(e);
    };
    
    const addEventListener: AddEventListenerType = (eventType: keyof EventMap, listener: (e: Event) => void) => {
      const callback = createFactory(listener);
    
      switch(eventType) {
        case 'Mouse':
          callback<typeof eventType>({ x: 1, y: 1, timestamp: 1 });
          break;
        case 'Keyboard':
          callback<typeof eventType>({ keyCode: 1, timestamp: 1 });
          break;
        default:
          const n: never = eventType;
          console.log(`${n} undefined`);
          break;
      }
    };
    
    addEventListener('Mouse', (e) => console.log(e.x, e.y));
    addEventListener('Keyboard', (e) => console.log(e.keyCode));

可以参考:https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts

  • LeetCode 题目
type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T];

type Async2Sync<T> = T extends ((input: Promise<infer X>) => Promise<Action<infer Y>>)
  ? (input: X) => Action<Y> 
  : (T extends (action: Action<infer W>) => Action<infer V> ? ((action: W) => Action<V>) : never);

type Connect = (module: EffectModule) => { [P in FuncName<typeof module>]: Async2Sync<typeof module[P]>}

interface Action<T> {
  payload?: T;
  type: string;
}

const connect: Connect = (m) => ({
  asyncMethod: (input: number) => {
    return {
      payload: `hello ${input}!`,
      type: 'delay'
    };
  },

  syncMethod: (action: Date) => {
    return {
      payload: action.getMilliseconds(),
      type: "set-message"
    };
  }
});

class EffectModule {
  count = 1;
  message = "hello!";

  asyncMethod(input: Promise<number>) {
    return input.then(i => ({
      payload: `hello ${i}!`,
      type: 'delay'
    }));
  }

  syncMethod(action: Action<Date>) {
    return {
      payload: action.payload!.getMilliseconds(),
      type: "set-message"
    };
  }
}

const effectModule = new EffectModule();
const connected = connect(effectModule);
  • 构建函数实例化入参定义
interface LogType {
  type: string;
  data: Record<string, any>
}

function normalizeParamsHandler(a: string, b: number): LogType {
  // dosomething
  return { type: 'click', data: { a: '1', b: 2 } };
}

function specialParamsHandler(a: string, b: number, c: boolean): LogType {
  // dosomething
  return { type: 'click', data: { a: '1', b: 2, c: true } };
}

function createFactory<T>(handler: (...args: any[]) => LogType) {
  type ParamType = T extends ((...args: any[]) => LogType)
    ? Parameters<T>
    : never;

  return function(...args: ParamType) {
    const logData = handler(...args);
    // dosomething
  };
}

const normalizeFactory = createFactory<typeof normalizeParamsHandler>(normalizeParamsHandler);
const specailParamsHandler = createFactory<typeof specialParamsHandler>(specialParamsHandler);

九、参考资料

  1. Typescript 文档版本
  2. typescript高级用法之infer的理解与使用
  3. 柯里化函数怎么转成TypeScript版
  4. TypeScript Deep Dive
  5. TypeScript Deep Dive 中文版
  6. [TypeScript中的never类型具体有什么用? - 尤雨溪的回答](http://7. https://www.zhihu.com/question/354601204/answer/888551021)
  7. 2020你必须准备的50道Typescript面试题
  8. Typescript有什么冷门但是很好用的特性?
  9. Typescript Handbook
  10. Typescript 的 extends
  11. 聊聊TypeScript类型兼容,协变、逆变、双向协变以及不变性