TypeScript 你学废了吗

702 阅读8分钟

TypeScript 你学废了吗

前言

TypeScript到目前为止,语言特性已经相当多相当复杂了。本文档着重于介绍那些特别有用、如果灵活运用会极大提高coding experience的语言特性。为了做到尽可能的覆盖大部分有用知识点,特性只会做简单的介绍,在实际的编码中可以慢慢积累这些类型的妙用。

Generic (范型)

范型形如下

function firstElement<Type>(arr: Type[]): Type {
  return arr[0];
}
const s = firstElement(["a", "b", "c"]);
// s is of type 'string'
const n = firstElement([1, 2, 3]);
// n is of type 'number'

推断

function map<I, O>(arr: I[], func: (arg: I) => O): O[] {
  return arr.map(func);
}

map函数根据传入的参数,动态的推断I,O的类型

类型约束

function strOrNum<T extends (number | string)>(input: T): T {
    return input
}
// 传入的参数得是 string 或者是 number

范型类型

范型更广阔使用领域为范型类型

interface Box<T> {
 content: T;
}
type Box<T> = T | Array<T>

Function Overloads (函数重载)

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678); // correct
const d2 = makeDate(5, 5, 5); // correct
const d3 = makeDate(1, 3); // error!

上面函数有三种重载签名,函数调用若均不符合其中任意一种函数签名,则会报错。

this的声明

this必须声明在函数参数声明中的第一个,且this在函数参数中的声明,不作为形参和实参。

class Handler {
    info: string | undefined
    handle(this: Handler, msg: string) {
      [this](http://this.info/)[.info](http://this.info/) = msg
    }
}
const h = new Handler()
h.handle('hello');

特殊类型

void

void 表示不返回值的函数的返回值。另外返回类型为 void 的上下文类型不会强制函数不返回某些内容。 另一种说法是具有 void 返回类型(类型 vf = () => void)的上下文函数类型,在实现时,可以返回任何其他值,但会被忽略。

function noop() {
  return;
}
// function noop(): void


type voidFunc = () => void;
const func: voidFunc = () => {
  return true;
};
const value = func();
// const value: void
// 即使func函数有返回值,也不影响,value的类型为void

这种行为存在,因此即使 Array.prototype.push 返回一个数字并且 Array.prototype.forEach 方法需要一个返回类型为 void 的函数,以下代码也是有效的。

const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));

unknown

unknown类似any,但是比any安全,因为通过unknown做任何操作都是不合法的。因此尽量用unknown,不用any。 这里结合typescirpt4.0中一个新的特性做个例子。

try {
    // do something crazy here
} catch(e) {
    console.error(e.message, 'error occur')
    // 默认情况下,这里的e是any类型
}

try {
    // do something crazy here
} catch(e: unknown) {
    console.error(e?.message, 'error occur')
    // typescript 4.0 之后 catch 这里的e支持标示类型。因此更安全的做法是使用unknown来定义e 不然any就相当于裸奔。很容易出现type error
}

never

never 类型表示从未观察到的值。 在返回类型中,这意味着函数抛出异常或终止程序的执行。

function fail(msg: string): never {
 throw new Error(msg)
}

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!
  }
}

Declaration Merging (声明合并)

Interface合并

interface Box {
 height: number;
 width: number;
}
interface Box {
 weight: number;
}
let box: Box = {height: 4, with: 5, weight: 6}

同名的interface会被合并

type 以及 interface的 区别: interface 可以被 extends , 但是 type 不行, interface 是一个对象(而且目前支持的应该只有 []{} ), 但是 type 可以玩出花来的。。因为 interface 本质上是需要支持 {} | [] 的一些约束的

Namespaces合并

和interface一样,namespaces也会合并。namespace中可以包含类型以及值。

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

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

如果遇到重复命名的类型或者值并且均导出的,在合并中会报错。(即使是相同命名的namespaces,没有导出的值或者类型在另外一个中是无法访问的)

Assertion & type checker (断言相关) & type narrowing

type narrowing

首先来了解一下 type narrowing Type narrowing的方式很多,主要有如下方式

  • typeof type guard

  • Truthiness narrowing

  • Equality narrowing

  • The in operator narrowing

  • instanceof narrowing

typeof 以及 instanceof

使用typeof以及instanceof关键词判断的方式也可以收敛类型。

function strOrNum(input: string | number) {
  if (typeof input === 'string') {
    // input is string now
  } else {
    // input is number
  }
}

Truthiness 和 equality

这两种方式也很类似,因此合并来讲。

declare const obj: Record<string, unknown> | null
declare const bool1: boolean | undefined
declare const bool2: boolean | null

if (obj) {
    // obj: Record<string, unknown>
} else {
    // obj: null
}

if (obj) {
    // obj: Record<string, unknown>
} else {
    // obj: null
}

if (bool1 === bool2) {
    // bool1: boolean
    // bool2: boolean
}

in 操作符

in操作符对于判断对象是否包含某一属性或者方法是很有效的,typescript中可以利用in操作符达到type-narrrowing的效果

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

as

有好几种断言方式。最简单一种是使用 as 关键词。

declare const str: any
let res = (str as string).toUpperCase()

declare const num: number
let res2 = (num as unknown as string).toLowerCase()
// num类型不能直接收敛的情况下,需要先将num断言为更宽泛的类型 例如unknown

Type Guards 类型守卫

可以通过自己实现类型守卫函数来将类型收敛性。类型守卫比typeof更加强大了。

type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
// ---cut---
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

let pet = getSmallPet();

if (isFish(pet)) {
  // pet类型被收敛到了类型 Fish 上
  pet.swim();
} else {
  pet.fly();
}

断言函数 assert functions

如果发生意外,有一组特定的函数会抛出错误。 它们被称为“断言”函数。 例如,Node.js 有一个专门的函数,称为assert。

function multiply(x: any, y: any) {
  assert(typeof x === "number");
  assert(typeof y === "number");

  // both x and y is number now
  return x * y;
}

Typescript中可以自定义实现assert函数。有两种形式的assert函数定义方式

第一种

function myAssert(param: any, msg?: string): asserts param {
  if (! param) {
    throw new Error(msg);
  }
}

function multiply(x: any, y: any) {
  myAssert(typeof x === "number");
  myAssert(typeof y === "number");
  // both x and y is number now
  return x * y;
}

第二种

declare function isOdd(param: unknown): asserts param is 1 | 3 | 5;
function num(input: number) {
  isOdd(input)
  console.log(input)
  // input is 1 | 3 | 5
}

! 后缀

!后缀可以从类型中排除掉null和undefined类型

declare const obj: {name: string} | null
const name = obj!.name // obj被断言为了 {name: string}

考虑到type安全,尽量不要用这个标识符。

第三方库实现的type assertion

GitHub - unional/type-plus: Additional types and types adjusted utilities for TypeScript

Mapped Types

in 标识符

映射类型是一种泛型类型,它使用 PropertyKeys 的联合(通常通过 keyof 创建)来迭代键以创建类型。 in标识符会遍历Type,返回key

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

+ - 标识符

  • 表示增加属性,- 表示消去属性。+ 符号可以不写,但是写上提高可读性。具体例子如下
type Student = {
  id: string;
  readonly name?: string;
};
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// {
//    id: string;
//    name?: string;
// }
type CreateImmutable<Type> = {
  +readonly [Property in keyof Type]: Type[Property];
};
// {
//    readonly  id: string;
//    readonly  name?: string;
// }
type CreateOptional<Type> = {
  [Property in keyof Type]+?: Type[Property];
};
// {
//    id?: string;
//    readonly  name?: string;
// }
type CreateRequired<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
// {
//    id: string;
//    readonly  name: string;
// }

as

TypeScript4.1 新增符号,配合下文中将要提到的Template Literal Types(模板文字类型)功能强大。

type MappedType<Type> = {
    [Properties in keyof Type as (string | number)]: Type[Properties]
}

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};

Conditional Type

如下形式为conditional type。可以做很多骚操作的关键。

SomeType extends OtherType ? TrueType : FalseType;

Recursive Conditional Types

typescript4.1 引入。有了recursive conditional types,各种循环迭代就可以支持了。有了这个特性支持可以写很多高级类型。

type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;

如果递归深度超过50层会报错。这点还是特别nice的。

Template Literal Types

同样是 typescript4.1 引入。

type World = "world";

type Greeting = `hello ${World}`;
// "hello world"
type EnumNum = 1 | 2 | 3
type EnumGroup = `${EnumNum}${EnumNum}`
// "11" | "12" | "13" | "21" | "22" | "23" | "31" | "32" | "33"
type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};

declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.on("firstNameChanged", newName => {
    //                        ^ string
    console.log(`new name is ${newName.toUpperCase()}`);
});

person.on("ageChanged", newAge => {
    //                  ^ number
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

有了template literal types 可以实现很多之前实现不了的type 下面这例子是实现camelCase转换

type ToCamel<S extends string> =
    S extends `${infer Head}-${infer Tail}`
    ? `${Lowercase<Head>}${Capitalize<ToCamel<Tail>>}`
    : S extends Uppercase<S>
    ? Lowercase<S>
    : S extends `${Capitalize<infer Head>}${Capitalize<infer Tail>}`
    ? S
    : void
type ToCamelCase<S extends string | number | bigint> = Uncapitalize<ToCamel<S & string>>

type T1 = ToCamelCase<"camel-case"> // camelCase
type T2 = ToCamelCase<"camelCase"> // camelCase
type T3 = ToCamelCase<"CamelCase"> // camelCase
type T4 = ToCamelCase<"CAMELCASE"> // camelcase

Variadic Tuple Types (可变元组类型)

Typescript4.0新增特性。

type Foo<T extends unknown[]> = [string, ...T, number];

type T1 = Foo<[boolean]>;  // [string, boolean, number]
type T2 = Foo<[number, number]>;  // [string, number, number, number]
type T3 = Foo<[]>;  // [string, number]
export type FirstElem<T extends readonly unknown[]> = T[0];
export type LastElem<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;

export type RemoveFirst<T extends readonly unknown[]> = T extends readonly [unknown, ...infer U] ? U : [...T];
// [unknown, ...infer U] 可以替换为 [any, ...infer U]
export type RomoveLast<T extends readonly unknown[]> = T extends readonly [...infer U, unknown] ? U : [...T];
// [...infer U, unknown] 可以替换为 [...infer U, any]

type TS1 = FirstElem <[number, string, boolean]>;  // number
type TS2 = LastElem <[number, string, boolean]>;  // boolean
type TS3 = RemoveFirst <[number, string, boolean]>;  // [string, boolean]
type TS4 = RomoveLast <[number, string, boolean]>;  // [number, string]

来看一个实际例子。有一个函数merge,实现的功能是merge输入的object。可以输入2个至任意多个object。来看看这种type是如何实现的。

type Merge<F extends Record<string | number, unknown>, S extends Record<string | number, unknown>> = {
  [P in keyof F | keyof S]: P extends keyof Omit<F, keyof S> ? F[P] : P extends keyof S ? S[P] : never;
};

type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
  ? {}
  : T['length'] extends 1
  ? T[0]
  : T extends [infer P, infer Q, ...infer U]
  ? P extends Record<string | number, unknown>
    ? Q extends Record<string | number, unknown>
      ? U extends Record<string | number, unknown>[]
        ? MergeArr<[Merge<P, Q>, ...U]>
        : MergeArr<[Merge<P, Q>]>
      : never
    : never
  : never;

declare function mixin<
  T extends Record<string | number, unknown>,
  P extends [Record<string | number, unknown>, ...Record<string | number, unknown>[]]
>(target: T, ...rest: P): MergeArr<[T, ...P]>

 const a = mixin({name: 'joe'}, {sex: 'male'}, {haveFun: [1,2,3]})
 // {name: string; sex: string; haveFun: number[]} 
 
 /*
** 这里写得这么麻烦全是因为typescript不够智能 **
按理来说 下面的代码已经足够了,但typescirpt报错
type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
  ? {}
  : T['length'] extends 1
  ? T[0]
  : T extends [infer P, infer Q, ...infer U]
  ? U extends Record<string | number, unknown>[]
    ? MergeArr<[Merge<P, Q>, ...U]>
    : MergeArr<[Merge<P, Q>]>
  : never;

** 另外 如果说typescript支持下面这种写法 也应该可以避免相当多冗余代码 但是typescript依然不支持
type MergeArr<T extends Record<string | number, unknown>[]> = T['length'] extends 0
  ? {}
  : T['length'] extends 1
  ? T[0]
  : T extends [infer P, infer Q, ...infer U]
  ? U extends Record<string | number, unknown>[]
    ? [P, Q] extends [Record<string | number, unknown>, Record<string | number, unknown>]
      ? MergeArr<[Merge<P, Q>, ...U]>
      : never
    : MergeArr<[Merge<P, Q>]>
  : never;
*/

Infer

上文中出现了infer标识符,infer标识符含义是根据输入的值去推断infer所在位置的type的类型。 这个概念刚开始可能比较抽象,看多了代码就会理解了。具体说明转 官网

// tuple中使用
type InferSecondElem<T extends unknown[]> = T extends [unknown, infer P, ...unknown[]] ? P : void

type A = InferSecondElem<[number, boolean, unknown, string]> // boolean

// template中使用
type Split<S extends string, D extends string> =
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
    [S];

type B = Split<'split,by,comma', ','> // ["split", "by", "comma"]

// function type中使用
type RetType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

type Res = RetType<() => number> // number

Type utils

官方提供了部分常用的utility types。通常大部分都在typescript包下的lib.es5.d.ts文件中。另外社区也有不少的封装。例如 这里 。可以实现加减乘除等各种骚操作。