TS 高级类型

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

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

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

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

类型系统的好处

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

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

sql 复制代码
function add(a, b) {
  return a + b
}

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

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

ts 复制代码
function add(a: number, b: number): number {
  return a + b
}

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

需要注意,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 用于分别定义类型和接口。

以下是简单的例子:

typescript 复制代码
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 关键字定义了用户的数据类型结构。

泛型

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

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

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

typescript 复制代码
// 非泛型写法:需为不同类型编写重复逻辑
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类型

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

ts 复制代码
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 中,并没有明确限制。接口可以定义字段集合,类型也可以定义一组方法。

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

类型约束

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

ts 复制代码
function CloneObject<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj))
}

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

ts 复制代码
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 等等。

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

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

交叉类型

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

ts 复制代码
type a = { a: 1 }
type b = { b: 2 }

type c = a & b

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

联合类型

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

ts 复制代码
type a = 1
type b = 2
type c = a | b

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

类型保护

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

ts 复制代码
function isString(data: unknown): data is string {
  if (typeof data === 'string') {
    return true
  }

  return false
}

索引类型

允许为类型设置索引。

ts 复制代码
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 初学者容易出现的问题。

ts 复制代码
type Json = {
  [x: string]: string
}

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

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

类型映射

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

ts 复制代码
// 内置工具类型
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 
};

条件类型

ts 复制代码
type IsString<T> = T extends string ? T : never

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

模板字面量

ts 复制代码
type Url = `${'http'|'https'}://${string}.com`

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

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

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

模板字面量的类型推断

ts 复制代码
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

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

示例

限制数字范围

ts 复制代码
/** @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

切割字符串为数组

ts 复制代码
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']

限制字符串开头和结尾

ts 复制代码
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

解析路由

ts 复制代码
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: ''
}

解析值

ts 复制代码
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'

解析路由参数

ts 复制代码
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]
}
相关推荐
乘风gg30 分钟前
还在养虾吗?虾王已诞生:微信龙虾 ClawBot
前端·ai编程·claude
小小小小宇1 小时前
LLM 长期记忆构建
前端
lichenyang4531 小时前
从 Express 老项目到 NestJS + Docker:一次车辆管理系统的渐进式重构
前端
Momo__2 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富2 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇2 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇2 小时前
React中的forwardRef
前端·react.js·面试
Flynt2 小时前
装上TypeScript 7.0 RC之后,最让我意外不是10倍提速
typescript·visual studio code
疯狂SQL2 小时前
手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
typescript·json·next.js·web worker·前端性能优化·esbuild·源码实战
槑有老呆2 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端