Typescript - 进阶篇

· 11 分钟阅读
  • 对基础篇的补充
  • 泛型的使用方法和场景
  • 常用泛型工具
  • 配合 React 使用实例

三斜杠 + reference 导入#

将 TypeScript 类型声明拆分。使用 三斜杠指令 + reference 引用可以大大缩短构建和编辑器的交互时间,强制组件之间的逻辑分离,并以新的和改进的方式组织代码

相关文档:https://www.typescriptlang.org/docs/handbook/project-references.html

/// <reference path="xxx.ts" />

从文件中直接引用类型#

xxx.ts
// ./xxx.tsexport type TypeName = {  // ...}
type T = import('./xxx.ts').TypeName
  • 这种引用方式不需要在文件头部写 import 声明,也不需要将类型导入全局。
  • 主要优势是可以快速导入单个类型,但如果需要导入同一文件中的多个类型,还是推荐使用 import 声明。

namespace 命名空间#

namespace 主要用于类型分组,也可用于配合 class(类) 声明类型。

官方文档:https://www.typescriptlang.org/docs/handbook/namespaces.html#namespacing

namespace Shapes {  export namespace Polygons {    export interface Triangle {}    export interface Square {}  }
  export interface Round {}}
const a: Shapes.Polygons.Triangle = {}

namespace 中的类型可以被重写

namespace Shapes {  export interface Round {    pi: number  }
  export namespace Polygons {    export interface Triangle {      side: 3    }  }}

在全局类型声明文件 (*.d.ts) 中 export 关键字不是必须的。

模板字符串类型#

本质上就是字符串类型,通过在模板字符串 (``) 中插入类型,遍历出一个字符串联合类型。

type T = {  a: number  b: number  c: string}
type T2 = 'x' | 'y' | 'z'
type T3 = `key-${keyof T}-${T2}-${number}`
// 结果:type T3 =  | `key-a-x-${number}`  | `key-a-y-${number}`  | `key-a-z-${number}`  | `key-b-x-${number}`  | `key-b-y-${number}`  | `key-b-z-${number}`  | `key-c-x-${number}`  | `key-c-y-${number}`  | `key-c-z-${number}`

主要作用就是对字符串联合做更精准的类型约束。

动态索引声明#

type T = {  [K: string]: unknown}

K 是索引签名参数,类型必须为 "string" 或 "number"。

K 不是固定名称,可以自定义语义话命名,如:

type T = {  [StringKeys: string]: unknown  [NumberKeys: number]: unknown}

keyof 将一个类型的索引映射为联合类型#

type T = {  a: unknown  b: unknown  c: unknown}
type TKeys = keyof T // => 'a' | 'b' | 'c'

注意:如果包含动态索引,可能无法推断出指理想的联合类型。

type T = {  [K: string]: unknown  a: unknown}
type TKeys = keyof T // => string | number
type T = {  [K: number]: unknown  a: unknown}
type TKeys = keyof T // => number | "a"

in#

in 关键字可以在索引中使用,用于映射需要推断的联合类型。

const obj = {  a: 1,  b: 2,  c: '3',}
type Keys = keyof typeof obj // 'a' | 'b' | 'c'
type T = {  [K in Keys]: typeof obj[K]}

类型 T 的推断结果:

type T = {  a: number  b: number  c: string}

索引引用#

type T = {  a: number  b: number  c: string}
type T2 = T['a']type T3 = T['c']
// T2 = number// T3 = string

声明合并#

将多个相同名称的类型合并。详情查看文档

interface Box {  height: number  width: number}
interface Box {  scale: number}
let box: Box = { height: 5, width: 6, scale: 10 }

以上内容是对基础篇的补充


泛型#

泛型就是可以传递参数并使用参数的类型。

泛型参数#

通过在类型名称后面添加一对 <> 来声明泛型参数。

interface RequestRes<T> {  /** 结果码:1000成功 其他情况参考码表 */  businessCode?: string  /** 返回对象 */  content?: T  /** 返回消息 */  message?: string  /** 响应描述 */  responseDes?: string  /** 子对象标识 */  subEchoToken?: string}
type TypeA = RequestRes<{}>// {//   businessCode?: string//   content?: {}//   message?: string//   responseDes?: string//   subEchoToken?: string// }
type TypeA = RequestRes<string[]>// {//   businessCode?: string//   content?: string[]//   message?: string//   responseDes?: string//   subEchoToken?: string// }

泛型参数可以有多个,使用逻辑与函数一致。

type TypeA<A, B, C> = {  a: A  b: B  c: C}

可以使用 = 赋予参数默认值,与 es6 语法中的函数默认值赋值方式一致。

type TypeA<T = unknown> = {}

没有默认值的泛型参数一律视为必填,如果在引用该泛型时没有传入,就会报错:

type TypeA<T> = {}type TypeB<T = unknown> = {}
type NewTypeA = TypeA // 报错type NewTypeB = TypeB // 不报错

在函数中使用泛型#

通过在函数声明的括号前添加一对 <> 来声明泛型参数,配合函数生命的泛型参数可以在函数体中引用。

function assign<T1, T2>(obj1: T1, obj2: T2): T1 & T2 {  return Object.assign(obj1, obj2)}
const a = assign({ a: 1 }, { b: '2' })
// 推断结果:// const a: {//     a: number;// } & {//     b: string;// }

函数中声明泛型参数与直接声明形参类型有什么区别?

  • 泛型参数可以被重复引用
  • 泛型参数可以在函数调用时传入
  • 在 IDE 中获得更好的类型推断

在什么情况下需要在函数中声明泛型?

  • 函数无法自动动推断类型时
  • 需要传入与函数形参无关的类型时
  • 函数体中需要多次引用参数类型时

函数中的泛型参数与函数形参没有强关联性:

function pick<T = unknown>(obj: unknown, pickedArr: string[]): T {  let result = Object.assign(obj)
  if (obj !== null && typeof obj === 'object') {    result = {}    for (const key of pickedArr) {      if (Object.prototype.hasOwnProperty.call(obj, key)) {        result[key] = obj[key]      }    }  }
  return result}
const obj = {  0: 1,  a: 1,  b: 2,  c: '3',}
// 调用 pick 函数时在括号前添加 <> 传入泛型参数const objH = pick<{ a: number; c: string }>(obj, ['a', 'c'])
// 结果:// const objH: {//   a: number//   c: string// }

在这个例子中泛型参数 T 与函数形参 obj 无关联,但被用作返回值的类型引用

泛型约束#

通过 extends 关键字可以对泛型参数进行类型约束,从而使该参数在后续使用和类型推断中更加精准

function pick<T extends Record<string | number, unknown>, K extends string[]>(obj: T, pickedArr: K): T {  let result: T = Object.assign(obj)
  if (obj !== null && typeof obj === 'object') {    result = {}    for (const key of pickedArr) {      if (Object.prototype.hasOwnProperty.call(obj, key)) {        result[key] = obj[key]      }    }  }
  return result}

上面这个例子中泛型参数 T 受到了类型 Record<string | number, unknown 的约束,那么 boj 参数在传入的时候必须满足该约束。

同理,pickedArr 参数在传入时需要满足 string[] 类型约束。

pick('aaa', ['a', 'c']) // 报错pick({ a: 1, b: 2, c: '3' }, 'a') // 报错pick({ a: 1, b: 2, c: '3' }, ['a', 'c']) // 通过

泛型约束与直接声明形参类型约束作用是相同的,区别在于泛型参数可以在函数体中的其他地方被引用。

不难发现上面这个 pick 方法的类型声明效果其实并不理想,我们希望该方法可以自动推断出传入的类型,pickedArr 的数组成员必须是 obj 中的已知属性,并且可以返回一个选取后的高精度类型。可以做以下优化:

function pick<T extends Record<string | number, unknown>, K extends keyof T>(obj: T, pickedArr: K[]): Pick<T, K> {  let result = Object.assign(obj)
  if (obj !== null && typeof obj === 'object') {    result = {}    for (const key of pickedArr) {      if (Object.prototype.hasOwnProperty.call(obj, key)) {        result[key] = obj[key]      }    }  }
  return result}

extends 三元判断#

例子 1:实现一个 If 泛型

type If<C extends boolean, T, F> = C extends true ? T : F
// type A = If<true, 'a', 'b'>  // => 'a'// type B = If<false, 'a', 'b'> // => 'b'

例子 2:当函数参数为 string 时返回值类型为 string ,否则返回 number

type TypeName<P> = (param: P) => P extends string ? string : number

infer#

infer 表示在 extends 条件语句中待推断的类型变量。

/** 获取 Promise 返回值 */type Awaited<T> = T extends Promise<infer U> ? U : never
/** 获取 Promise 返回值 (递归) */type AwaitedDeep<T> = T extends Promise<infer U> ? (U extends Promise<unknown> ? Awaited<U> : U) : never

内置泛型工具#

Partial - 将类型中所有属性转为非必填#

type T = {  a: string  b: string  c?: string}
type T2 = Partial<T>
// type T2 = {//   a?: string//   b?: string//   c?: string// }

Required - 将类型中所有属性转换为必填#

type T2 = Required<T>
// type T2 = {//   a: string//   b: string//   c: string// }

Readonly - 将类型中所有属性转换为只读#

type T2 = Readonly<T>
type T2 = {  readonly a: string  readonly b: string  readonly c?: string}

Pick - 从一个类型中选取某些属性得到一个新类型#

type T2 = Pick<T, 'a' | 'c'>
// type T2 = {//   a: string//   c: string// }

Omit - 从一个类型中排除某些属性得到一个新类型#

type T2 = Omit<T, 'a' | 'c'>
// type T2 = {//   b: string// }

Record - 构建一个对象类型#

type T2 = Record<string, unknown>
// type T2 = {//   [K:string]: unknown// }
type T2 = Record<keyof T, unknown>
// type T2 = {//   a: unknown//   b: unknown//   c: unknown// }

Parameters - 获取函数的参数类型#

type T2 = Parameters<typeof pick>
// type T2 = [obj: Record<string | number, unknown>, pickedArr: (string | number)[]]

ConstructorParameters - 获取一个类的构造函数的参数#

class ClassName {  constructor(props: Record<string, unknown>) {}}
type T2 = ConstructorParameters<typeof ClassName>
// type T2 = [props: Record<string, unknown>]

ReturnType - 获取函数返回值#

type T2 = ReturnType<typeof pick>
// type T2 = {//   [x: string]: unknown;//   [x: number]: unknown;// }

NonNullable - 从联合类型中排除 null 和 undefined#

type T = string | null | number | '1' | 2type T2 = NonNullable<T>
// type T2 = string | number

使用泛型工具实现类型重写#

type T = {  a: string  b: number}
interface T2 extends Omit<T, 'a'> {  a: string[]}
// T2 = {//   a: string[]//   b: number// }

一些自定义泛型工具#

数组转联合#

type TupleToUnion<T extends readonly unknown[]> = T[number]
// TupleToUnion<[1, 2, '3']> // expected to be 1 | 2 | '3'

获取数组长度#

type Length<T extends readonly unknown[]> = T['length']
// Length<[0, 0, 0]>  // expected 3

数组转对象#

type TupleToObject<T extends readonly unknown[]> = {  [P in T[number]]: P}
// TupleToObject<['a', 'b', 'c']> // expected {a:'a', b:'b', c:'c'  }

获取数组第一个元素#

type First<T extends readonly unknown[]> = T['length'] extends 0 ? never : T[0]
// First<['a', 'b', 'c']]> // expected to be 'a'

拼接两个数组#

type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
// Concat<['a', 'b'], ['c']]> // expected to be ['a', 'b', 'c']

Includes#

type Includes<T extends readonly unknown[], U> = U extends T[number] ? true : false

函数重载 (Overload)#

用于根据单个函数传入参数的不同来分配不同的返回值类型类型。

相关文档: https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads

例子:

interface ConfigType {  a?: string  b?: string  c?: number}
function getConfig(key?: keyof ConfigType) {  let config: ConfigType = {}
  try {    config = JSON.parse(localStorage.getItem('app-config'))  } catch {}
  if (key) {    return config[key]  } else {    return config  }}
  • 如果传入 key, 那么从对象 config 中取出并返回这个 key 对应的值。
  • 如果不传入, 那么将返回完整的 config 对象。

TS 可以自动推断出这个函数的返回值类型为:string | number | ConfigType, 但是这并不理想,我们希望它可以更精准一些。

这个时候就可以使用函数重载来实现, 如下所示:

interface ConfigType {  a?: string  b?: string  c?: number}
function getConfig(): ConfigTypefunction getConfig<T extends keyof ConfigType>(key: T): ConfigType[T]function getConfig(key?: keyof ConfigType) {  let config: ConfigType = {}
  try {    config = JSON.parse(localStorage.getItem('app-config'))  } catch {}
  if (key) {    return config[key]  } else {    return config  }}
getConfig() // function getConfig(): ConfigType (+1 overload)getConfig('a') // function getConfig<"a">(key: "a"): string (+1 overload)getConfig('c') // function getConfig<"c">(key: "c"): number (+1 overload)

引用内置类型#

tslib 中包含了大量的内置类型, 如 HtmlElement, Event, Window, WindowEventMap 等, 这些类型是针对 Javascript 基础语法的默认声明。

const el = document.querySelector('#id .class')

这行代码是获取某一个 DOM 元素,看上去没什么问题,但由于 DOM 元素的多样性,TS 无法定位到具体哪一个元素类型,所以变量 el 的推断类型默认是 Element, 如下图所示: 图 1
这个时候 el 在类型上并没有继承到 dom 的原型: 图 2
作为开发者,我们可以知道获取元素的具体类型,比如这个元素是一个 div, 那么就可以通过泛型参数的传递来告诉 TS 我要获取的元素的类型:

const el = document.querySelector<HTMLDivElement>('#id .class')

图 3

不同的元素类型不同,比如遇 input 元素:

const el = document.querySelector<HTMLInputElement>('#id .class')

接下来 el 将获得 input 元素特有的属性:

图 4

实例: 在 React 中使用#

import React from 'react'
export interface AppProps {  /** 展示状态 */  show?: boolean  /** 展示内容 */  content: React.ReactNode}
export interface AppState {  count: number}
export class App extends React.Component<AppProps, AppState> {  readonly state: AppState = {    count: 0,  }
  /** 定时器 */  private TM: NodeJS.Timeout
  constructor(props: AppProps) {    super(props)    this.TM = setInterval(() => {      this.setState({ count: this.state.count + 1 })    }, 1000)  }
  render() {    const { show, content } = this.props    const { count } = this.state
    if (!show) return null
    return (      <div>        {content} - {count}      </div>    )  }
  componentWillUnmount() {    clearInterval(this.TM)  }}

React 函数式组件:

import React from 'react'
export interface AppProps {  content: React.ReactNode}
export const App: React.FC<AppProps> = (props) => {  const [count, setCount] = React.useState<number>(0)
  return (    <div>      {props.content} - {count}    </div>  )}

高级注释#

ts 中的注释遵循 tsdoc 规范,并在 vscode 中支持 markdown 语法。

健全良好的注释可以提升代码的可维护性,例子:

import React from 'react'
interface Props {}interface State {}
/** * @module 组件名称 * * 组件的详细说明 bala bala ..... * * - [x] 功能点1:bala bala... * - [x] 功能点2:[一个链接](https://www.typescriptlang.org/docs/handbook/2/basic-types.html), 直接贴地址也可以 https://www.typescriptlang.org/docs/handbook/2/basic-types.html * - [ ] ~~功能点3:(已废弃) ...~~ * - [ ] ... * * - [ ] 测试用例1:..... * - [ ] 测试用例2:..... * - [ ] .... * * - [ ] BUG: xxxx http://jira.huazhu.com/xxxxx * * ![img](https://lanten.coding.net/p/image-hub/d/image-hub/git/raw/master/0/82d99852b7a58ef98279a8838bf5e28329d52068b07421ce897a7b00d834806a.gif?download=false) */export class App extends React.Component<Props, State> {  /** 渲染头部内容 */  renderHeader(): JSX.Element {    return <div>header</div>  }
  /**   * 方法说明 bala bala...   *   * @param text 相关参数说明   * @returns 返回一个 JSX Element   */  renderContent(text: string): JSX.Element {    return <div>content: {text}</div>  }
  /**   * 废弃方法,例如某些方法已经不再推荐使用,但是还需要兼容老代码的情况下使用   *   * @deprecated   */  renderFooter: () => null
  render() {    return (      <>        {this.renderHeader()}        {this.renderContent('ok')}        {this.renderFooter()}      </>    )  }}

unique 唯一值#

unique 关键字用于表示全局唯一值,unique 后面必跟 symbol unique unique

unique 非常特殊,即表示类型,又可以直接作为值来使用

官方文档:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html

// Worksdeclare const Foo: unique symbol// Error! 'Bar' isn't a constant.let Bar: unique symbol = Symbol()// Works - refers to a unique symbol, but its identity is tied to 'Foo'.let Baz: typeof Foo = Foo// Also works.class C {  static readonly StaticSymbol: unique symbol = Symbol()}

枚举类型 enum#

枚举是组织收集有关联变量的一种方式,许多程序语言(如:c/c#/Java)都有枚举数据类型。

相关文档:https://jkchao.github.io/typescript-book-chinese/typings/enums.html

enum CardSuit {  Clubs, // 0  Diamonds, // 1  Hearts, // 2  Spades, // 3}
// 简单的使用枚举类型let Card = CardSuit.Clubs // 0
// 类型安全Card = 'not a member of card suit' // Error: string 不能赋值给 `CardSuit` 类型

手动关联值:

enum CardSuit {  Clubs = 5, // 5  Diamonds, // 6  Hearts, // 7  Spades, // 8}
enum CardSuit2 {  Clubs, // 0  Diamonds = 5, // 6  Hearts, // 7  Spades, // 8}

在手动关联的值后面未关联的部分的值依次递增

class 类的语法扩展#

略...

官方文档:https://www.typescriptlang.org/docs/handbook/2/classes.html


总结#

  • 目前我个人所整理的 TS 类型系统相关的常用内容都在这里了。
  • TS 除了类型系统外,还有少量的运行时的语法扩展,比如枚举类型 enum, class 类的扩展等。
  • 通过在开发过程中查阅第三方库的类型声明,实现自我学习,无需刻意花时间查阅在线文档及教程。
  • 对于复杂类型接口的见识越广,那么面对一个陌生类库的难度就会越低,形成一个良性循环。
  • 灵活运用泛型,将可复用泛型抽象封装。
  • 尽可能收窄类型约束,提高类型精度,追求高质量代码。
  • 无关紧要的高精度声明不宜耗费太长时间,该偷懒的还是要偷懒,避免延误工期。

参考#

TS 练习题#