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> // 02. 分配行为
ts
// 当 T 是联合类型时,会对每个成员分别判断
type IsString<T> = T extends string ? true : false
type R = IsString<string | number>
// 等价于 IsString<string> | IsString<number>
// 最终是 true | false3. 关闭分配
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> // number3. 提取元组首元素
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()) // string2. 泛型优先还是重载优先
- 结构相同,仅类型不同。 优先泛型。
- 分支逻辑不同。 优先重载。
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。useUnknownInCatchVariables。catch变量默认是unknown,避免误用。
