TypeScript 实践与技巧

1,964 阅读7分钟

一、常用技巧

1. 函数重载

export function util(str: string): string
export function util(str: number): number
export function util(str: string | number): number | string {
  return str;
}

const a: string = util('a');
const b: number = util(1);

2. 捕获变量类型

通过 typeof 关键字捕获变量的类型

let a = 123;
let b = { x: 0, y: 1 };

type A = typeof a; // number 类型
type B = typeof b; // { x: number, y: number }

3 . 函数绑定 this 上下文

通过绑定函数的第一个参数 this 指定当前函数的上下文(官方文档

const obj = {
  say(name: string) {
    console.log('Hello,', name);
  }
}

// 通过第一个参数 this 绑定当前函数上下文
function foo(this: typeof obj, str: string) {
  this.say(str);
}

4. 使用 Never

Never 表示永远不会发生的类型(官方文档),比如抛错函数的返回、死循环函数的返回

// 抛出异常
function unexpected(): never {
  throw Error('Unexpected');
}

function test(x: boolean): number {
  if (x) {
    return 1;
  }
  unexpected(); // 因为 never 表示永远不会发生的类型,所以这里不会要求 number 作为返回值
}

// 官方文档给的死循环的例子
function infiniteLoop(): never {
  while (true) {}
}

5. 索引签名

interface A {
  x: number
}
const a: A = {x: 1}

// 接口键类型为: string, 值类型为: boolean
interface B {
  [key: string]: boolean
}
const b: B = {
  a: true,
  b: false
}

// in 表示遍历,键名称包括: 'a', 'b', 'c',值类型为: string
type C = {
  [key in 'a' | 'b' | 'c']: string
}
const c: C = {
  a: '1',
  b: '2',
  c: '3'
}

6. 通过索引访问类型

官方文档

interface A {
  a: string
  b: {
    c: boolean
  }
}

type Aa = A['a'] // string 类型
type Abc = A['b']['c'] // boolean 类型
// 提取元祖对应的类型
type B = [string, number]
type B0 = B[0] // string 类型
type B1 = B[1] // number 类型
// 提取数组的元素类型
type C = Array<string | number | boolean>

// 通过访问数组类型的任意下标获取数组元素类型
type D = C[0] // 类型 (string | number | boolean)

7. 使用 as

可以通过 as 关键字指定值的类型,可以很轻松的处理掉一些有歧义的场景

function foo(str?: string) {
  return str.toString();
}

const obj = {
  toString() {
  }
}
foo(obj as string)

8. 条件类型

顾名思义条件类型就是根据条件决定使用哪种类型(官方文档),条件类型大概长这样:T extends U ? X : Y

// 与 JS 的三元表达式很类似,如果 T 的类型是 string, 则返回 string 类型,否则,返回 number 类型
type F<T> = T extends string ? string : number

const a: F<string> = 'abc'; // 泛型的类型是 string, 则为 string 类型
const b: F<number> = 1; // 泛型的类型是非 string, 则为 number 类型
const c: F<boolean> = 2; // 泛型的类型是非 string, 则为 number 类型

9. 使用 is

关键字 is, 用于类型保护,我的理解就是更高级的 boolean

// 返回类型 boolean,而且告诉编译器 value 类型是 string 时,返回 true
function isString(value: any): value is string {
  return typeof value === 'string';
}

function fn(val: number | string) {
  if (isString(val)) { // 如果这里判断通过,表示 val 类型是 string
    val.trim();
  } else { // 否则,val 类型为 number
    val.toFixed(2);
  }
}

官方的 Array.isArray 方法定义就使用了 is 关键字:

interface ArrayConstructor {
  isArray: (arg: any) => arg is any[];
}

10. 通过修饰构造函数参数定义类属性

通过修饰构造函数的参数可以快捷定义类属性并自动赋值,看下面例子:

class Point {
  constructor(private x: number, private y: number) {
  }
}
class Point {
  private x: number
  private y: number
  
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

以上2种写法是等同的,但是上面的可以少写一些代码。除了用 private 修饰构造函数的参数,其他类似的修饰关键字还有: publicprotectedreadonly, 详见官方文档

二、泛型

泛型可以提供更灵活的类型约束,而不局限于某种类型。常用于 functionclassinterfacetype官方文档

1. 使用示例

function

function foo<T>(arg: T): T {
  return arg;
}

class

class Foo<T> {
  name: T

  constructor(name: T) {
    this.name = name;
  }

  getName(): T {
    return this.name
  }
}

const foo = new Foo<number>(3);
const name: number = foo.getName();

interface

interface Foo<T> {
  a: T
}

const foo: Foo<number> = { a: 1 }

type

type Foo<T> = { a: T }
const foo: Foo<boolean> = { a: false }

2. 泛型约束

可以通过 extends 关键字约束泛型

// 指定泛型的类型限制于: string | number, 其他类型将会报错
interface A<T extends string | number> {
  [key: string]: T
}

const a: A<string> = {a: 'abc'}
const b: A<number> = {a: 123}

3. 指定泛型默认类型

interface B<T = number> {
  [key: string]: T
}

const c: B = { a: 123 }
const d: B<string> = { a: 'abc' }

三、TypeScript 中的一些运算符

下面梳理了 TypeScript 中的一些运算符,对理解后面的内容非常有帮助

  • ?: 表示可选的参数或属性,可以用于函数参数、接口的属性等
  • -?: 取消可选标识(扩展:类似的还有 -readonly 表示取消 readonly 标识)
  • in: 表示遍历类型
  • typeof: 捕获变量类型,上面有例子
  • keyof: 将对象类型生成其键的字符串或数字的集合,常与 in 结合使用
  • infer: 表示推断类型,下面讲 TS 内置类型 Parameters 实现的时候会有具体例子

四、实现 TypeScript 内置类型

TypeScript 提供了一些常用的类型,开发者可以直接使用它们,下面看看这些类型是如何实现的(内置类型使用文档

1. Record

构造一个键为 K, 值为 T 的键值对类型

// keyof any 包含: string | number | symbol
type Record<K extends keyof any, T> = {
  // 表示键类型是约束于 K (string | number | symbol) 中的一种或多种,值类型为 T
  // in 表示遍历
  [P in K]: T
}

const foo: Record<string, boolean> = {
  a: true
}
const bar: Record<'x' | 'y', number> = {
  x: 1,
  y: 2
}

2. Partial

使 T 中的所有属性都变成可选的

type Partial<T> = {
  // 将原始类型 T 的所有属性加上 ? 修饰,变成可选的
  [P in keyof T]?: T[P]
}

interface Foo {
  a: string
  b: number
}

const foo: Partial<Foo> = {
  b: 2 // `a` 已经不是必须的了
}

3. Required

与 Partial 相反,将 T 中的所有属性变成必须的

type Required<T> = {
  // 将原始类型 T 的所有属性加上 -? 修饰,变成必须的
  // -? 表示移出 ? 这个标识
  [P in keyof T]-?: T[P]
}

interface Foo {
  a: string
  b?: number
}

const foo: Required<Foo> = {
  a: 'abc',
  b: 2 // `b` 已经变成必选的了
}

4. Readonly

使 T 中的所有属性变成只读

type Readonly<T> = {
  // 将原始类型 T 的所有属性加上 readonly 修饰,属性变成只读的
  readonly [P in keyof T]: T[P]
}

interface Foo {
  a: string
}

const foo: Readonly<Foo> = {
  a: 'abc'
}
// foo.a = 'def' // 只读,不可修改


// 此外,可以使用 -readonly 修饰,表示去掉 readonly 修饰
type Writable<T> = {
  -readonly [P in keyof T]: T[P]
}

interface Bar {
  readonly a: string
}

const bar: Writable<Bar> = {
  a: 'abc'
}
bar.a = 'def'; // `Bar['a'] 已经去掉了 readonly 修饰,可以修改了

5. Pick

从类型 T 中选择一些属性,这些属性来自于集合 K

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

interface Foo {
  a: string
  b: number
  c: boolean
}

const foo: Pick<Foo, 'b' | 'c'> = {
  b: 1,
  c: false
}

6. Exclude

排除掉 T 中能赋值给 U 的类型

// 如果 T 是 U 的子类型,则返回 never, 否则返回 T
type Exclude<T, U> = T extends U ? never : T

// 'a' | 'b' | 'c' 中排除掉 'b', 只能为 'a' 或 'c'
let foo: Exclude<'a' | 'b' | 'c', 'b'> = 'a'
foo = 'c'

7. Extract

与 Exclude 相反,提取 T 中能赋值给 U 的类型

// 如果 T 是 U 的子类型,则返回 T, 否则返回 never
type Extract<T, U> = T extends U ? T : never

// 'a' | 'b' | 'c' 中提取 'b', 只能为 'b'
let foo: Extract<'a' | 'b' | 'c', 'b'> = 'b'

8. Omit

省略掉 T 中的一些属性,这些属性来自于集合 K

// 1. 从 T 的属性中排除掉 K,得到剩下的一个属性集合
// 2. 从 T 中选择剩下的集合
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

interface Foo {
  a: string
  b: number
  c: boolean
}

// 从 Foo 中省略 'c', 接口 Foo 只剩下 'a' 和 'b'
let foo: Omit<Foo, 'c'> = {
  a: 'a',
  b: 1
}

9. NonNullable

排除掉 null 、undefined 类型

// 排除掉 null | undefined
type NonNullable<T> = T extends null | undefined ? never : T

type Foo = string | null
const a: NonNullable<Foo> = 'a' // 不能赋值给 null 或 undefined

10. Parameters

根据函数的参数,返回对应的元组类型

// 主要说一下 infer P, 在这里表示待推断的函数参数
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

type Foo = (a: string, b: number) => void
const a: Parameters<Foo> = ['a', 1] // 元组 [string, number]

11. ConstructorParameters

根据构造函数的参数,返回对应的元组类型

// 跟 Parameters 类似
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never

interface Foo {
  new(a: string, b: number)
}

const a: ConstructorParameters<Foo> = ['a', 1] // 元组 [string, number]

12. ReturnType

返回函数的返回类型

// 跟 Parameters 类似
type ReturnType<T extends (...args: any) => any> = T extends (...args:any) => infer R ? R : any

type Foo = () => boolean
const a: ReturnType<Foo> = true // 返回 boolean 类型

13. InstanceType

// 跟 Parameters 类似
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any

参考链接