TS 高级类型

TS,全称 TypeScript,是 JavaScript 的超集。

它在 JavaScript 的基础上引入了静态类型系统。通过在代码中标注类型,可以帮助开发者在编译阶段就可以发现错误,提高代码可读可维护性。

并且类型检查仅在编译时进行,在运行时,类型会被擦除,也就是说,TS 并不会引入额外的运行时开销。

由于 TS 完全兼容 JS,也就是说,所有合法的 JS 代码在 TS 中都是合法的。因此,如果你已经掌握了 JS,那么学习 TS 就如同在已有基础上增加类型支持,门槛并不高。

类型系统的好处

我们都知道,JavaScript 是一门动态类型语言,变量的类型可以在运行时随时改变,这赋予了它极大的灵活性。

但这种灵活性也带来了不小的隐患。例如,某个函数期望接收两个数字作为参数,但调用时却传入了字符串,由于没有类型约束,这种错误只能在运行时才会暴露出来:

function add(a, b) {
  return a + b
}

add(1, '2') // 结果为 '12',不是预期中的 3

为了避免这类错误,TypeScript 引入了静态类型系统,通过在代码中标注类型,使得可以在开发阶段就发现潜在问题,提升代码的健壮性与可维护性。

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

在上面的例子中,如果传入非 number 类型的参数,TS 会立刻报错。

需要注意,TS 的类型系统只在编译时生效,不会引入任何运行时代码或开销。这意味着,类型信息在编译之后会被擦除,不会影响运行时性能。

TS 采用的是结构化类型系统,也称为“鸭子类型”。

如果一个东西看起来像鸭子,叫起来像鸭子,走起来也像鸭子,那么它就是鸭子。

换句话说,只要一个对象的结构符合某个类型的定义,就可以被视为该类型,无需显式声明。

type Point = { x: number; y: number }

const p1: Point = { x: 10, y: 20 } // 合法

const obj = { x: 5, y: 15, z: 25 }
const p2: Point = obj // 合法,因为结构匹配

这种灵活性让 TS 既具备类型系统的安全性,也保留了 JS 的表达能力。

类型系统的使用

在 TS 中,提供了关键字 typeinterface 用于分别定义类型和接口。

以下是简单的例子:

type UserID = number

type UserInfo = {
  id: UserID
  name: string
  age: number
}

type UserInfoHandle = (info: UserInfo) => UserInfo

interface UserInfoInterface {
  id: UserID
  name: string
  age: number
}

interface UserInfoInterfaceHandle {
  (info: UserInfoInterface): UserInfoInterface
}


const userInfo: UserInfo = {
  id: 1,
  name: '1',
  age: 1
}

const userInfoInterfaceHandle: UserInfoInterfaceHandle = info => info

const userInfoInterface: UserInfoInterface = userInfo

const userInfoHandle: UserInfoHandle = userInfoInterfaceHandle

以上例子分别使用 typeinterface 关键字定义了用户的数据类型结构。

泛型

泛型是类型系统中不可或缺的角色。

它允许我们编写可以适用于多种类型的代码,而不必重复编写相同的逻辑。

泛型通过类型参数来实现,这些参数在定义时并不指定具体类型,而是在使用时由调用者指定。

// 非泛型写法:需为不同类型编写重复逻辑
function identityNumber(arg: any, defaultVal: number): number { return arg ?? defaultVal }
function identityString(arg: any, defaultVal: string): string { return arg ?? defaultVal }

// 泛型写法:通过类型参数T实现类型抽象
function identity<T>(arg: any, defaultVal: T): T {
  return arg ?? defaultVal
}

// 使用时动态指定类型
const num = identity<number>(100, 100)      // number类型
const str = identity<string>('hello', 'hello')  // string类型

// 类型推论场景(TS自动推导类型)
const bool = identity(true, true)            // boolean类型

泛型可以广泛应用在类型系统中。

interface Data<T> {
  value: T
}

type Route<Req, Res> = {
  request: Req
  response: Res
}

function Clone<T>(data: T): T {
  return JSON.parse(JSON.stringify(data))
}

class Item<T> {
  constructor(
    public readonly value: T
  ) {}
}

class ExtItem<T> extends Item<T> implements Data<T> {
  constructor(
    public readonly value: T
  ) {
    super(value)
  }
}

const route: Route<'req', 'res'> = { request: 'req', response: 'res' }
const route_copy = Clone(route)
const item1 = new Item<typeof route>(route)
const item2 = new ExtItem<boolean>(false)

接口和类型

通常来说,接口用于定义一组方法,类型用来定义一些字段集合。

但在 TS 中,并没有明确限制。接口可以定义字段集合,类型也可以定义一组方法。

interface IFather {
  type: string
  eat(food: string): void
}

interface IChild extends IFather {
  name: 'child'
  drink(water: string): void
}

type TFather = {
  type: string
  eat(food: string): void
}

type TChid = {
  name: 'child'
  drink(water: string): void
} & TFather

const f1: IFather = {
  type: 'father',
  eat(food) {
    console.log("eat", food)
  }
}

const c1: IChild = {
  name: 'child',
  type: 'child',
  eat(food) {
    console.log("eat", food)
  },
  drink(water) {
    console.log("drink", water)
  },
}

class Father implements IFather{
  constructor(
    public readonly type: string
  ) {}

  eat(food: string): void {
    console.log("eat", food)
  }
}

const f2: TFather = f1

const c2: TChid = c1

类型约束

很多时候,我们希望类型可以更加具体,而不是广泛。

function CloneObject<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj))
}

CloneObject 为例,我们可能只希望克隆对象,但是实际上可以 CloneObject(1), CloneObject('hello') 克隆基本类型。

function CloneObj<T extends Record<string, string>>(obj: T): T {
  return JSON.parse(JSON.stringify(obj))
}

T extends Record<string, string> 将泛型 T 约束为匹配 Record<string, string> 类型。

类型体操

类型体操,是对于 TS 的类型操作的一种调侃。

在 TS 中,存在有多种内置的类型工具。

有将对象属性全部转换为可选的 Partial 和全部转为必须的 Required

从对象中取出指定键集合的 Pick,忽略指定键的 Omit ,乃至于将字符串转换大小写的 Uppercase, Lowercase, Capitalize, Uncapitalize 等等。

通过类型体操,可以实现复杂类型的定义。

但是正常业务开发中是不会有太复杂的类型体操的。它的应用场景通常是库和框架开发。

交叉类型

用来将类型组合,而不是重复代码。

type a = { a: 1 }
type b = { b: 2 }

type c = a & b

const v: c = { a: 1, b: 2 }

联合类型

限制类型在指定类型集合中。

type a = 1
type b = 2
type c = a | b

const va: c = 1
const vb: c = 2

类型保护

将类型从未知变成已知的。

function isString(data: unknown): data is string {
  if (typeof data === 'string') {
    return true
  }

  return false
}

索引类型

允许为类型设置索引。

type List = {
  [index: number]: number
}

type obj = {
  [key: string]: 'a' | 'b' | 'c'
}

const list = [1, 2, 3] as const
const l: List = list
const el: typeof list[number] = 1

const o: obj = {
  'a': 'a',
  'b': 'b',
  'c': 'c'
}

const val: obj[keyof obj] = 'b'
const len: typeof list['length'] = 3

关于索引类型有一个 TS 初学者容易出现的问题。

type Json = {
  [x: string]: string
}

通常这个要表示 JSON 是使用 Record 这个 TS 内置的类型工具的。

以上示例等价于 Record<string, string>

类型映射

通过映射类型索引来重新定义类型。

// 内置工具类型
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 自定义映射类型
type Nullable<T> = { 
  [P in keyof T]: T[P] | null 
};

条件类型

type IsString<T> = T extends string ? T : never

如果传入的 T 匹配 string 那么匹配返回 T,否则返回 never

模板字面量

type Url = `${'http'|'https'}://${string}.com`

type Url = `${'http' | 'https'}://${string}.com`

const google: Url = 'https://google.com'
const example: Url = 'http://example.com'

要求字符串匹配定义格式。

模板字面量的类型推断

type TransformBool<S extends string> = S extends `${string}=${infer b}`
  ? b extends 'true'
  ? true
  : b extends 'false'
  ? false
  : never
  : never

const f: TransformBool<'a=false'> = false
const t: TransformBool<'b=true'> = true

从模板字面量中提取指定内容,随后使用条件类型。

示例

限制数字范围

/** @description (0, N] */
type Enumerate<N extends number, A extends number[] = []> = A['length'] extends N ? A[number] : Enumerate<N, [...A, A['length']]>

/** @description (S, E] */
type IntRange<S extends number, E extends number> = Exclude<Enumerate<E>, Enumerate<S>>

const fromZeroToTen: Enumerate<11> = 1

const inFiveToTen: IntRange<5, 11> = 5

切割字符串为数组

type Split<P extends string, S extends string, Strict = true> = P extends `${infer p}${S}${infer component}`
  ? Strict extends true
    ? component extends ''
      ? [p]
      : [p, ...Split<component, S, Strict>]
    : [p, ...Split<component, S, Strict>]
  : [P]

const a: Split<'a-b-', '-', false> = ['a', 'b', '']
const b: Split<'a-b-', '-', true> = ['a', 'b']

限制字符串开头和结尾

type StartsWith<S extends string, M extends string> = S extends `${M}${string}` ? true : false

type EndsWith<S extends string, M extends string> = S extends `${string}${M}` ? true : false

const str = 'aaabbbccc'
const start: StartsWith<typeof str, 'aaa'> = true
const end: EndsWith<typeof str, 'ccc'> = true

解析路由

type AllPath<P extends string> = P extends `${infer p}/${infer component}` ? Record<p, string> & AllPath<component> : Record<P, string>

type FilterParamPath<T extends Record<string, string>, K extends keyof T = keyof T> = K extends `:${infer p}`
  ? p extends `${infer param}?${string}` ? param : p
  : never

type ParamPath<AllPath extends Record<string, string>> = {
  [P in FilterParamPath<AllPath>]: string
}

const p = '/v1/:perfix/major/minor/:patch/:username/publish/:id?tag=1&tag=2&tag=3&category=5&mode=dart&publish=false&list=1,2,3,4,5'

const param: ParamPath<AllPath<typeof p>> = {
  id: '',
  patch: '',
  perfix: '',
  username: ''
}

解析值

type ParseList<A extends unknown[], Strict = true> = A[number] extends string ? ParseValue<A[number], Strict> : A[number]

type ParseValue<V extends string, Strict = true> =
  V extends 'true' ? Strict extends true ? true : boolean :
  V extends 'false' ? Strict extends true ? false : boolean :
  V extends `${infer N extends number}` ? Strict extends true ? N : number :
  V extends `null` ? null :
  V extends `undefined` ? undefined :
  V extends `${string},${string}` ? ParseList<Split<V, ','>, Strict>[] :
  V

const n: ParseValue<'123'> = 123
const f: ParseValue<'false'> = false
const b: ParseValue<'true'> = true

const list: ParseValue<'a,b,c,d,e'> = ['a', 'b', 'c', 'd', 'e']

const elements: ParseList<typeof list> = 'a'

解析路由参数

type QueryUnit<P extends string> = P extends `${infer k}=${infer v}` ? Record<k, ParseValue<v>> : never

type MergeQuery<A extends Record<string, unknown>, B extends Record<string, unknown>> = {
  [K in (keyof A | keyof B)]:
  K extends keyof A
  ? K extends keyof B
  ? [A[K], B[K]]
  : A[K]
  : K extends keyof B
  ? B[K]
  : never
}

type QueryHandle<P extends string> = P extends `${infer q}&${infer querys}` ? MergeQuery<QueryUnit<q>, QueryHandle<querys>> : P extends `${infer q}&` ? QueryUnit<q> : QueryUnit<P>

type FlatListEl<A extends unknown> = A extends Array<infer els>
  ? els extends unknown[]
  ? FlatListEl<els>
  : els
  : A

type FlatList<A extends unknown, Els = FlatListEl<A>> = [Els] extends [A] ? A : Els[]

type Query<P extends string> = P extends `${string}?${infer query}` ? QueryHandle<query> : never

type Querys<P extends string, O extends Record<string, unknown> = Query<P>> = {
  [K in keyof O]: FlatList<O[K]>
}

const p = '/v1/:perfix/major/minor/:patch/:username/publish/:id?tag=1&tag=2&tag=3&category=5&mode=dart&publish=false&list=1,2,3,4,5'

const query: Querys<typeof p> = {
  'tag': [1, 2],
  'category': 5,
  'mode': 'dart',
  publish: false,
  list: [1, 2, 3, 4, 5]
}

TS 高级类型
http://www.inksha.com/archives/ts-gao-ji-lei-xing
作者
inksha
发布于
2025年06月19日
许可协议