Skip to content

03 - TS 进阶

一、类型运算的底层思路

TS 进阶的核心不是记语法。 而是把类型看成一套可计算的数据。

  • 联合类型像集合并集。
  • 交叉类型像集合交集。
  • 条件类型像类型层 if。
  • 映射类型像类型层循环。
ts
// 联合类型:成员是候选集合
type A = 'a' | 'b' | 'c'

// 交叉类型:把多个约束叠加到同一个对象
type UserBase = { id: number }
type UserMeta = { name: string }
// 两边字段会被合并
type User = UserBase & UserMeta

// 条件类型:按条件输出不同类型
// 这里是分配式条件类型
type ToArray<T> = T extends any ? T[] : never

这个阶段一个常见误区。 把类型当“注释”。 实际上类型本身也能做计算。


二、条件类型与分配行为

1. 条件类型基础

ts
// T 能赋值给 U,则返回 X,否则返回 Y
type If<T, U, X, Y> = T extends U ? X : Y

// string 能赋值给 string
// 所以结果是 1
type A = If<string, string, 1, 0> // 1
// number 不能赋值给 string
// 所以结果是 0
type B = If<number, string, 1, 0> // 0

2. 分配行为

ts
// 当 T 是联合类型时,会对每个成员分别判断
type IsString<T> = T extends string ? true : false

type R = IsString<string | number>
// 等价于 IsString<string> | IsString<number>
// 最终是 true | false

3. 关闭分配

ts
// 把 T 放进元组,避免分配行为
type IsAllString<T> = [T] extends [string] ? true : false

// 这里是整体判断
// 不是逐个成员判断
type R1 = IsAllString<string | number> // false
type R2 = IsAllString<string> // true

这种写法在封装工具类型时很重要。 尤其是判断“整体是否满足”时。


三、infer 类型推断

infer 常用于从复杂类型中“拆结构”。

1. 提取函数返回值

ts
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type Fn = (id: number) => { ok: boolean; code: number }
// infer R 会抓到函数返回值
type FnResult = MyReturnType<Fn>
// { ok: boolean; code: number }

2. 提取 Promise 内部类型

ts
type MyAwaited<T> = T extends Promise<infer R> ? R : T

// Promise<string> 命中左分支
// 拿到内部 string
type A = MyAwaited<Promise<string>> // string
// number 不命中左分支
// 直接返回原类型
type B = MyAwaited<number> // number

3. 提取元组首元素

ts
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never

// 第一个元素被推断为 1
type H1 = Head<[1, 2, 3]> // 1
// 空元组不命中模式
// 返回 never
type H2 = Head<[]> // never

四、映射类型与键重映射

1. 基础映射

ts
interface Profile {
	id: number
	name: string
	active: boolean
}

// 把每个字段都变成可选
type MyPartial<T> = {
	// K 会遍历 keyof T
	// 每个字段都加问号
	[K in keyof T]?: T[K]
}

type PartialProfile = MyPartial<Profile>

2. 只读与去只读

ts
// 加 readonly
type MyReadonly<T> = {
	// 所有字段都变为只读
	readonly [K in keyof T]: T[K]
}

// 去 readonly
type Mutable<T> = {
	// -readonly 是移除修饰符
	-readonly [K in keyof T]: T[K]
}

3. 键重映射 as

ts
interface ApiUser {
	id: number
	user_name: string
	is_active: boolean
}

// 把 snake_case 键映射为更可读的前缀形式
type PrefixKeys<T, P extends string> = {
	// as 后面是键重映射
	// value 类型保持不变
	[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]
}

type U = PrefixKeys<ApiUser, 'api'>
// 得到:apiId apiUser_name apiIs_active

键重映射适合做“DTO 到 ViewModel”这类过渡类型。


五、模板字面量类型

模板字面量类型可以拼接字符串类型。 适合规范事件名 路由名 权限码。

ts
type Module = 'user' | 'order'
type Action = 'create' | 'delete' | 'update'

// 拼出统一权限标识
// 两个联合会做笛卡尔积
type PermissionCode = `${Module}:${Action}`

const p1: PermissionCode = 'user:create'
// const p2: PermissionCode = 'user:remove' // 报错,不在集合里
ts
type Field = 'name' | 'email' | 'mobile'
type ChangeEvent = `${Field}Changed`

function emitEvent(event: ChangeEvent): void {
	// 这里只演示类型约束
	console.log(event)
}

emitEvent('nameChanged')
// emitEvent('nameChange') // 报错,命名不符合约束

六、函数重载与泛型设计

1. 什么时候用重载

输入与输出关系明显分支化时。 重载比联合类型更清晰。

ts
// 重载签名
function formatValue(value: number): string
function formatValue(value: Date): string

// 实现签名
function formatValue(value: number | Date): string {
	if (typeof value === 'number') {
		// number 分支
		return value.toFixed(2)
	}
	// Date 分支
	return value.toISOString()
}

// 调用点会命中不同重载签名
const a = formatValue(3.14159) // string
const b = formatValue(new Date()) // string

2. 泛型优先还是重载优先

  • 结构相同,仅类型不同。 优先泛型。
  • 分支逻辑不同。 优先重载。
ts
// 泛型场景:输入数组类型不同,但规则一致
function first<T>(arr: T[]): T | undefined {
	return arr[0]
}

// 重载场景:返回值形态随参数分支变化明显
function parse(v: string): number
function parse(v: number): string
function parse(v: string | number): number | string {
	return typeof v === 'string' ? Number(v) : String(v)
}

七、工具类型进阶组合

ts
interface KuroUser {
	id: number
	name: string
	age: number
	email: string
	createdAt: string
}

// 创建参数:去掉系统字段
type CreateUserDTO = Omit<KuroUser, 'id' | 'createdAt'>

// 更新参数:允许部分更新,但 name 仍必填
// 先取 name 必填
// 再把其他字段变可选
type UpdateUserDTO = Pick<KuroUser, 'name'> & Partial<Omit<KuroUser, 'name'>>

// 列表展示:只要轻量字段
type UserListItem = Pick<KuroUser, 'id' | 'name' | 'email'>

// 只读快照:禁止误改
type UserSnapshot = Readonly<KuroUser>

八、类型体操实战

1. 深度只读

ts
type DeepReadonly<T> = {
	readonly [K in keyof T]:
		T[K] extends (...args: any[]) => any
			? T[K] // 函数保持原样,不做递归
			: T[K] extends object
				? DeepReadonly<T[K]> // 对象递归只读
				: T[K] // 原始值直接返回
}

interface State {
	user: {
		id: number
		profile: { name: string }
	}
	update: (name: string) => void
}

type ReadonlyState = DeepReadonly<State>

2. 深度可选

ts
type DeepPartial<T> = {
	[K in keyof T]?:
		T[K] extends object
			? DeepPartial<T[K]> // 递归把每层都变可选
			: T[K]
}

interface Query {
	pagination: {
		page: number
		pageSize: number
	}
	filters: {
		keyword: string
		status: 'all' | 'active'
	}
}

const q: DeepPartial<Query> = {
	filters: { keyword: 'kuro' }
}

3. 提取对象值类型

ts
const STATUS = {
	Idle: 'idle',
	Loading: 'loading',
	Success: 'success',
	Error: 'error'
} as const

// 取对象 value 联合类型
// 先拿键
// 再按键索引
type Status = typeof STATUS[keyof typeof STATUS]

const s1: Status = 'loading'
// const s2: Status = 'pending' // 报错

九、声明文件与模块扩展

1. 什么时候需要 d.ts

  • JS 库没有类型声明
  • 需要补充全局变量类型
  • 需要扩展第三方模块类型

2. 全局声明示例

ts
// global.d.ts
declare global {
	interface Window {
		KURO_ENV: 'dev' | 'prod'
	}
}

export {}

3. 模块扩展示例

ts
// axios-augment.d.ts
import 'axios'

declare module 'axios' {
	interface AxiosRequestConfig {
		retry?: number // 给请求配置补一个重试字段
	}
}

十、tsconfig 严格模式补充

除了 strict。 下面几项也很关键。

json
{
	"compilerOptions": {
		"strict": true,
		"noUncheckedIndexedAccess": true,
		"exactOptionalPropertyTypes": true,
		"noImplicitOverride": true,
		"noPropertyAccessFromIndexSignature": true,
		"useUnknownInCatchVariables": true
	}
}
  • noUncheckedIndexedAccess。 索引访问默认带上 undefined
  • exactOptionalPropertyTypes。 可选属性语义更精确。
  • noImplicitOverride。 重写父类方法必须显式 override
  • useUnknownInCatchVariablescatch 变量默认是 unknown,避免误用。

<< 返回首页