TypeScript 高级类型系统详解:从泛型到工具类型的实战指南

作者:函数茶馆

时间:2026-10-23

浏览:118

本文深入解析 TypeScript 高级类型系统,涵盖泛型定义与约束、交叉与联合类型的合并逻辑、类型映射与条件类型的应用,以及 Partial、Required、Pick 等常用工具泛型的实战用法,帮助开发者摆脱 AnyScript,构建类型安全的复杂应用。

文章目录

TypeScript 高级类型系统详解

前言

对于具备 JavaScript 开发经验的开发者而言,入门 TypeScript 并非难事,只需掌握其基础类型系统,即可逐步将 JavaScript 应用迁移至 TypeScript 环境。


// js
const double = (num) => 2 * num

// ts
const double = (num: number): number => 2 * num

然而,随着应用逻辑日益复杂,开发者往往倾向于将变量直接标记为 any 类型,导致 TypeScript 代码退化为 AnyScript。为了深入理解 TypeScript 的类型系统并避免这一陷阱,本文将重点介绍高级类型特性,协助大家构建更严谨的代码。

泛型基础与类型推导

泛型

在探讨高级类型之前,有必要先厘清泛型的基本概念。

泛型是强类型编程语言中的核心概念之一,合理使用泛型能够显著提升代码的可复用性,使系统架构更加灵活。维基百科对泛型的定义如下:

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型通过一对尖括号来表示( <> ),尖括号内的字符被称为类型变量,该变量用于表示具体的类型。


function copy<T>(arg: T): T {
 if (typeof arg === 'object') {
 return JSON.parse(
  JSON.stringify(arg)
 )
 } else {
 return arg
 }
}

在调用 copy 函数之前,这个类型 T 是不确定的;只有当函数被调用时,我们才能确定 T 具体代表何种类型。


const str = copy<string>('my name is typescript')

TypeScript 高级类型系统详解

在 VS Code 中观察 copy 函数,可以看到其参数及返回值已具备类型信息,这意味着调用 copy 函数时,实际上是为类型变量 T 赋予了 string 类型。值得注意的是,调用 copy 时可以省略尖括号,TypeScript 的类型推导机制足以确定 T 为 string。

TypeScript 高级类型系统详解

交叉与联合类型的应用

高级类型

除了 string、number、boolean 等基础类型外,还需掌握类型声明中的一些高级用法。

交叉类型(&)

交叉类型的本质是将多个类型合并为单一类型,更准确地说应称为「合并类型」,其语法规则与逻辑“与”符号一致。

T & U

假设现有两个类:Button(按钮)和 Link(超链接),若需创建一个兼具按钮和超链接功能的组件,即可利用交叉类型来实现。


interface Button {
 type: string
 text: string
}

interface Link {
 alt: string
 href: string
}

const linkBtn: Button & Link = {
 type: 'danger',
 text: '跳转到百度',
 alt: '跳转到百度',
 href: 'http://www.baidu.com'
}

联合类型(|)

联合类型的语法规则与逻辑“或”符号一致,表示该变量的类型可以是连接符连接的多个类型中的任意一个。

T | U

例如,对于之前的 Button 组件,其 type 属性仅能指定固定的几种字符串值。


interface Button {
 type: 'default' | 'primary' | 'danger'
 text: string
}

const btn: Button = {
 type: 'primary',
 text: '按钮'
}

类型别名与索引机制

类型别名(type)

若前文提到的交叉类型与联合类型在多处复用,可通过类型别名为其命名。类型别名的声明语法与变量类似,只需将 const、let 替换为 type 关键字即可。

type Alias = T | U


type InnerType = 'default' | 'primary' | 'danger'

interface Button {
 type: InnerType
 text: string
}

interface Alert {
 type: ButtonType
 text: string
}

类型索引(keyof)

keyof 类似于 Object.keys,用于获取接口中所有 Key 的联合类型。


interface Button {
 type: string
 text: string
}

type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"

仍以之前的 Button 类为例,其 type 类型源自另一个类 ButtonTypes。若按传统写法,每次 ButtonTypes 更新都需手动修改 Button 类;而使用 keyof 则可避免此类维护烦恼。


interface ButtonStyle {
 color: string
 background: string
}
interface ButtonTypes {
 default: ButtonStyle
 primary: ButtonStyle
 danger: ButtonStyle
}
interface Button {
 type: 'default' | 'primary' | 'danger'
 text: string
}

// 使用 keyof 后,ButtonTypes修改后,type 类型会自动修改 
interface Button {
 type: keyof ButtonTypes
 text: string
}

类型约束与映射技巧

类型约束(extends)

此处的 extends 关键词不同于 class 继承中 extends 的作用,在泛型中使用的主要目的是对泛型参数施加约束。以前文编写的 copy 方法为例:


type BaseType = string | number | boolean

// 这里表示 copy 的参数
// 只能是字符串、数字、布尔这几种基础类型
function copy<T extends BaseType>(arg: T): T {
 return arg
}

TypeScript 高级类型系统详解

若传入对象类型,则会出现问题。

TypeScript 高级类型系统详解

extends 经常与 keyof 配合使用。例如,若有一个专门用于获取对象值的方法,但对象结构不确定,即可利用 extends 和 keyof 进行约束。


function getValue<T, K extends keyof T>(obj: T, key: K) {
 return obj[key]
}

const obj = { a: 1 }
const a = getValue(obj, 'a')

TypeScript 高级类型系统详解

此处的 getValue 方法能够根据传入的参数 obj 来约束 key 的有效值。

条件类型与工具泛型实战

类型映射(in)

in 关键词主要用于类型映射,可遍历已有接口的 key 或联合类型。以下使用内置的泛型接口 Readonly 为例进行说明。


type Readonly<T> = {
 readonly [P in keyof T]: T[P];
};

interface Obj {
 a: string
 b: string
}

type ReadOnlyObj = Readonly<Obj>

TypeScript 高级类型系统详解

我们可以拆解该逻辑:首先通过 keyof Obj 获取联合类型 'a' | 'b'。


interface Obj {
 a: string
 b: string
}

type ObjKeys = 'a' | 'b'

type ReadOnlyObj = {
 readonly [P in ObjKeys]: Obj[P];
}

随后,P in ObjKeys 相当于执行了一次 forEach 逻辑,遍历 'a' | 'b'。


type ReadOnlyObj = {
 readonly a: Obj['a'];
 readonly b: Obj['b'];
}

最终即可得到一个新的接口定义。


interface ReadOnlyObj {
 readonly a: string;
 readonly b: string;
}

条件类型(U ? X : Y)

条件类型的语法规则与三元表达式一致,常用于处理类型不确定的场景。

T extends U ? X : Y

上述逻辑意为:如果 T 是 U 的子集,则返回类型 X,否则返回类型 Y。以下使用内置的泛型接口 Extract 为例进行说明。


type Extract<T, U> = T extends U ? T : never;

若 T 中的类型在 U 中存在,则予以返回,否则予以抛弃。假设有两个类,拥有三个公共属性,可通过 Extract 提取这三个公共属性。


interface Worker {
 name: string
 age: number
 email: string
 salary: number
}

interface Student {
 name: string
 age: number
 email: string
 grade: number
}


type CommonKeys = Extract<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

TypeScript 高级类型系统详解

工具泛型

TypeScript 内置了大量工具泛型,前文已介绍过 Readonly、Extract 两种。这些内置泛型均定义在 TypeScript 内置的 lib.es5.d.ts 中,因此无需任何额外依赖即可直接使用。以下列举一些常用工具泛型。

TypeScript 高级类型系统详解

Partial


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

Partial 用于将接口的所有属性设置为可选状态。其实现首先通过 keyof T 取出类型变量 T 的所有属性,然后通过 in 进行遍历,最后在属性后添加一个 ?。

在使用 TypeScript 编写 React 组件时,若组件属性均存在默认值,即可通过 Partial 将所有属性值变为可选。


import React from 'react'

interface ButtonProps {
 type: 'button' | 'submit' | 'reset'
 text: string
 disabled: boolean
 onClick: () => void
}

// 将按钮组件的 props 的属性都改为可选
const render = (props: Partial<ButtonProps> = {}) => {
 const baseProps = {
 disabled: false,
 type: 'button',
 text: 'Hello World',
 onClick: () => {},
 }
 const options = { ...baseProps, ...props }
 return (
 <button
  type={options.type}
  disabled={options.disabled}
  onClick={options.onClick}>
  {options.text}
 </button>
 )
}

Required


type Required<T> = {
 [P in keyof T]-?: T[P]
}

Required 的作用与 Partial 恰好相反,旨在将接口中所有可选属性改为必填。其区别仅在于将 Partial 内部的 ? 替换为 -?。

Record


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

Record 接受两个类型变量,Record 生成的类型具有类型 K 中存在的属性,其值为类型 T。此处有一个易混淆点:给类型 K 添加类型约束 extends keyof any,我们可以先观察 keyof any 的具体含义。

TypeScript 高级类型系统详解

其含义大致为:类型 K 被约束在 string | number | symbol 中,这恰好对应对象的索引类型,即类型 K 只能指定为这几种类型。

在业务代码中,常需构造某个对象的数组,但数组不便索引,因此有时会将对象的某个字段提取出来作为索引,进而构造新对象。假设有一个商品列表数组,需查找商品名为「每日坚果」的商品,通常需遍历数组,较为繁琐。为简化操作,可将该数组改写为对象形式。


interface Goods {
 id: string
 name: string
 price: string
 image: string
}

const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')

goodsList.forEach(goods => {
 goodsMap[goods.name] = goods
})

Pick


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

Pick 主要用于提取接口的特定属性。开发 Todo 工具时可知,编辑时需填写描述信息,而预览时仅显示标题和完成状态。因此,可通过 Pick 工具提取 Todo 接口的两个属性,生成新类型 TodoPreview。


interface Todo {
 title: string
 completed: boolean
 description: string
}

type TodoPreview = Pick<Todo, "title" | "completed">

const todo: TodoPreview = {
 title: 'Clean room',
 completed: false
}

TypeScript 高级类型系统详解

Exclude


type Exclude<T, U> = T extends U ? never : T

Exclude 的作用与前文介绍的 Extract 恰好相反:若 T 中的类型在 U 中不存在,则返回,否则抛弃。现以之前的两个类为例,观察 Exclude 的返回结果。


interface Worker {
 name: string
 age: number
 email: string
 salary: number
}

interface Student {
 name: string
 age: number
 email: string
 grade: number
}


type ExcludeKeys = Exclude<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

TypeScript 高级类型系统详解

其结果为取出 Worker 在 Student 中不存在的 salary。

Omit


type Omit<T, K extends keyof any> = Pick<
 T, Exclude<keyof T, K>
>

Omit 的作用与 Pick 相反,先通过 Exclude<keyof T, K> 取出类型 T 中存在但 K 不存在的属性,再由这些属性构造新类型。仍以前面的 Todo 案例为例,TodoPreview 类型只需排除接口的 description 属性即可,写法较之前的 Pick 更为精简。


interface Todo {
 title: string
 completed: boolean
 description: string
}

type TodoPreview = Omit<Todo, "description">

const todo: TodoPreview = {
 title: 'Clean room',
 completed: false
}

TypeScript 高级类型系统详解

总结

若仅掌握 TypeScript 的基础类型,难以在开发中游刃有余。此外,TypeScript 4.0 版本新增更多功能,想要充分利用它,必须持续学习与掌握。希望阅读本文的读者能有所收获,彻底摆脱 AnyScript 的困扰。

到此这篇关于你不知道的 TypeScript 高级类型(小结)的文章就介绍到这了,更多相关TypeScript 高级类型内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章