一句话总结
TypeScript 的进阶价值,是把"运行时的约束"提前到编译期:类型体操让你用类型本身表达业务逻辑的不变量(例如"这个函数的返回值一定是数组元素类型"),而工程化配置(strict / 声明文件 / 类型测试)则让大型项目在持续演化中始终保持类型安全。本篇不堆砌语法,而是从"为什么这样设计"出发,带你把基础篇没讲透的泛型、条件类型、infer、模板字面量,以及落地到团队的工程化手段串成一条线。
阅读建议:初级/中级偏理解,高级偏"类型即程序",实战偏团队落地。面试只考三分之一,但能写出高级章节的类型,基本能秒杀绝大多数 TS 进阶面。
初级:类型系统巩固
进阶题翻车,八成是基础语义没吃透。先补齐四个最容易混淆的点。
1. type 与 interface:什么时候用哪个
两者都能描述对象形状,但有本质差异:
- interface 支持声明合并(多次声明自动合并),适合描述"对象/类契约",可被
class implements; - type 是类型别名,能做联合
A | B、交叉A & B、映射类型、条件类型,灵活度更高,但不能声明合并。
// interface:可合并,适合公开 API 契约
interface User {
id: number;
name: string;
}
interface User { // 合并:补充字段
email: string;
}
// type:做组合更自然
type ID = number | string;
type Admin = User & { role: 'admin' };
经验法则:能用 interface 就用 interface(报错信息更友好、可被扩展);需要联合/交叉/工具类型时用 type。
2. 联合类型 vs 交叉类型
A | B表示"或"——值可以是 A 或 B,访问属性时只能访问两者共有的;A & B表示"且"——值必须同时满足 A 和 B,属性合并;若同名属性类型冲突,结果变为never。
3. 字面量类型与类型收窄(narrowing)
TS 能在 if 分支里把"宽类型"收窄成"窄类型"。常用收窄手段:=== 字面量比较、typeof、in、instanceof,以及判别联合(discriminated union)。
type Shape =
| { kind: 'circle'; r: number }
| { kind: 'rect'; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) { // 判别属性 kind 让 TS 精确收窄
case 'circle': return Math.PI * s.r ** 2;
case 'rect': return s.w * s.h;
}
}
4. 枚举 vs const 断言字面量联合
普通 enum 会生成运行时对象且可被反向映射,带来体积与语义成本;优先用 as const 字面量联合,零运行时成本、类型更精确。
// 不推荐:有运行时对象
enum Status { Ok = 1, Err = 2 }
// 推荐:纯类型,零运行时
const Status = { Ok: 1, Err: 2 } as const;
type Status = typeof Status[keyof typeof Status]; // 1 | 2
中级:泛型与映射/条件类型
泛型是"类型层面的函数",映射/条件类型是它的两个核心算子。
1. 泛型函数、约束与 keyof / 索引访问
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // 返回值类型精确为 T[K]
}
interface User { id: number; name: string; age: number }
const u: User = { id: 1, name: 'a', age: 20 };
const n: number = pick(u, 'age'); // ✅ K 被约束为 'id'|'name'|'age'
// pick(u, 'email'); // ❌ 报错:'email' 不在 keyof User
keyof T 取所有键的联合,T[K] 做索引访问——这是所有工具类型的基石。
2. 映射类型(Mapped Types)
遍历一个类型的所有键,生成新类型:{ [K in keyof T]: T[K] }。内置 Partial/Required/Readonly/Record 都是它的语法糖。
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
3. 条件类型与分布式分发
T extends U ? X : Y 像三元表达式。当 T 是裸类型参数(未被数组/元组/Promise 等包裹)且为联合类型时,会自动分发到每个成员上。
type IsString<T> = T extends string ? true : false;
type R = IsString<string | number>; // true | false(分发到两个成员)
若想关闭分发,用方括号包住 T:[T] extends [U] ? X : Y。
4. 手写 MyPick / MyReadonly(实战入门)
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
高级:类型体操实战
这一节的写法是"用类型写一个程序":输入一个类型,输出另一个类型。面试常考,工程中用于把数据库行类型精确投射到 DTO。
1. infer:在条件类型里"捕获"片段
infer 让你在 extends 的"模式"里挖个坑,把匹配到的类型片段存成变量。
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<number[]>; // number
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type B = UnwrapPromise<Promise<string>>; // string
2. 递归条件类型
深度 Partial、元组转联合都靠递归。注意 TS 4.5+ 支持,且递归深度有上限。
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type UnionFromTuple<T extends unknown[]> =
T extends [infer First, ...infer Rest]
? First | UnionFromTuple<Rest>
: never;
3. 模板字面量类型(Template Literal Types)
把字符串类型当模板拼装,配合 Capitalize/Uppercase 等内置工具,可生成"事件名""路由路径"这类派生字符串类型。
type EventName<K extends string> = `on${Capitalize<K>}`;
type Click = EventName<'click'>; // "onClick"
type Path<T, K extends keyof T> = K extends string ? `GET /${K}` : never;
4. 手写内置工具类型全家桶
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T;
5. this 类型与多态 this(链式调用)
class Builder {
private items: string[] = [];
add(x: string): this { this.items.push(x); return this; } // 返回 this 类型
}
class Sub extends Builder { tag() { return 'sub'; } }
new Sub().add('a').tag(); // ✅ 链式后仍保留 Sub 类型
实战:工程化落地
类型体操再花哨,落不到工程里都是空中楼阁。这一节讲团队真正用得上的配置与流程。
1. tsconfig 精要
"strict": true:开启strictNullChecks/noImplicitAny/strictFunctionTypes等一篮子检查,是类型安全的地基;"moduleResolution": "bundler":配合 Vite/webpack5,支持package.json exports与省略扩展名;"paths"+"baseUrl":路径别名@/...,记得同步打包器与tsconfig;"verbatimModuleSyntax": true:强制区分import type与值导入,利于 tree-shaking 与隔离编译。
2. 声明文件 .d.ts 与 declare
给无类型的 JS、全局变量或第三方黑盒补类型,避免到处 any。
// legacy-lib.d.ts
declare module 'legacy-lib' {
export function doThing(input: string): number;
}
declare const __APP_VERSION__: string; // 全局注入的版本号
3. 与 ESLint / 打包器协作
用 typescript-eslint 统一规则(如 no-explicit-any、consistent-type-definitions),把"禁用 any""统一 type/interface 风格"写成 CI 门禁,而不是靠口头约定。
4. 类型测试:把类型当断言
公共类型/DTO 一旦改坏,下游全崩。用 vitest 的 expectTypeOf 或 tsd 写"类型单测"。
import { expectTypeOf } from 'vitest';
expectTypeOf(pick(u, 'age')).toEqualTypeOf<number>();
expectTypeOf(MyOmit<User, 'age'>).not.toEqualTypeOf<User>();
5. 渐进迁移策略
- 纯 JS 仓库:加
"allowJs": true+"checkJs": true,再用// @ts-check逐文件开启; - Vue/Svelte:组件
<script lang="ts">渐进替换; - 老库无类型:局部
// @ts-expect-error标注并登记 TODO,而非全局any。
面试模拟
Q1:type 和 interface 怎么选?
公开对象契约、需要被类 implements 或可能扩展的用 interface;做联合/交叉/工具类型组合时用 type。两者大部分场景可互换,团队的 consistent-type-definitions 规则应统一一种为主。
Q2:什么是分布式条件类型?为什么联合会分发?
当条件类型的检查对象是裸类型参数且为联合时,TS 会把联合"拆开"逐成员计算再合并。用 [T] extends [U] 包裹可关闭分发,得到整体判断结果。
Q3:infer 解决了什么问题?手写取 Promise 值的类型。
infer 在模式匹配中捕获子类型。答案:type Awaited<T> = T extends PromiseLike<infer V> ? Awaited<V> : T;(递归处理嵌套 Promise)。
Q4:如何避免 any 扩散?unknown 和 any 的区别?
any 关闭所有检查、会"泄漏"到下游;unknown 是"类型安全版的 any",使用前必须收窄。策略:默认 unknown,开 noImplicitAny,对第三方黑盒用 .d.ts 收口。
Q5:strict 包含哪些关键检查?为什么建议全开?
核心有 strictNullChecks(消灭 null/undefined 空指针类错误)、noImplicitAny、strictFunctionTypes(函数参数逆变检查)、noImplicitThis 等。全开能在编译期拦截大量运行时崩溃。
Q6:类型体操在实际项目中该用到什么程度?
原则:让类型服务于业务不变量,而不是炫技。DTO 投射、判别联合建模、配置对象的精确类型是高 ROI 场景;过度递归/深层模板字面量会拖慢编译且难维护,应克制并用类型测试守护。