类型安全的 API 设计
##核心问题解析
问题一:怎么让 API 用错就报错?
我们希望 API 能在编译时发现错误,而不是运行时报错。
不安全的 API:
ts
function setMode(mode: string) {
// 无法限制合法值
}
setMode('dark'); // ✅ 正确
setMode('light'); // ✅ 正确
setMode('dakr'); // ❌ 拼写错误,但 TS 不报错!解决方案:使用字面量类型或联合类型约束取值:
ts
type ThemeMode = 'light' | 'dark';
function setMode(mode: ThemeMode) { /* ... */ }
setMode('dakr'); // ❌ 编译错误!设计原则:让非法状态无法被表示(Make Impossible States Unrepresentable)
问题二:如何设计“链式调用”的类型?
链式调用(Fluent API)常见于构建器模式:
ts
db
.where('age', '>', 18)
.select('name', 'email')
.orderBy('name')
.exec();如果类型设计不当,.exec() 后可能还能继续调用 .where()。
答案:使用泛型 + 条件类型控制状态流转。
问题三:可选配置项怎么避免误配?
配置对象容易出错:
ts
fetch('/api', {
methood: 'POST', // ❌ 拼写错误
headers: { 'Content-Type': 'application/json' }
});TS 默认不会报错,因为 methood 被视为额外属性(除非开启 noExtraProperties)。
解决方案:
- 使用
as const提升类型精度 - 使用精确对象类型 + 泛型约束
- 利用
keyof和映射类型防止拼写错误
学习目标详解
目标一:使用字面量类型约束参数取值
1. 字符串字面量类型
ts
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
function request(url: string, method: HTTPMethod) { /* ... */ }
request('/user', 'GET'); // ✅
request('/user', 'PATCH'); // ❌ 编译错误2. 数字字面量类型
ts
type StatusCode = 200 | 404 | 500;
function sendStatus(code: StatusCode) { /* ... */ }3. 布尔字面量(较少用)
ts
function configure(flag: true | false) { /* ... */ }结合 enum 或 as const 可生成字面量类型。
目标二:实现函数重载以支持多态调用
函数重载允许一个函数有多个调用签名。
场景:createUser 支持多种参数形式
ts
// 重载签名
function createUser(name: string): User;
function createUser(email: string, name: string): User;
function createUser(id: number, email: string, name: string): User;
// 实现签名(更宽泛)
function createUser(arg1: any, arg2?: any, arg3?: any): User {
if (typeof arg1 === 'number') {
return { id: arg1, email: arg2, name: arg3 };
} else if (typeof arg2 === 'string') {
return { id: Date.now(), email: arg1, name: arg2 };
} else {
return { id: Date.now(), name: arg1, email: '' };
}
}
// 使用
createUser('Alice'); // ✅
createUser('alice@email.com', 'Alice'); // ✅
createUser(1, 'alice@email.com', 'Alice'); // ✅优势:
- 类型安全
- IDE 自动提示所有重载形式
- 实现逻辑集中
目标三:设计泛型 + 条件类型驱动的 fluent API
实现一个类型安全的查询构建器。
ts
class QueryBuilder<T, Stage extends 'initial' | 'where' | 'select' | 'done'> {
// 私有构造函数,控制实例创建
private constructor() {}
// 初始状态
static initial<T>() {
return new QueryBuilder<T, 'initial'>() as any;
}
// 添加 where 条件
where<K extends keyof T>(
key: K,
op: '=' | '>' | '<',
value: T[K]
): QueryBuilder<T, 'where'> {
// ...逻辑
return this as any;
}
// 添加 select 字段
select<K extends keyof T>(
...keys: K[]
): QueryBuilder<T, 'select'> {
return this as any;
}
// 执行查询,进入完成状态
exec(): Promise<Pick<T, any>> {
return Promise.resolve({} as any);
}
}
// 使用
interface User {
id: number;
name: string;
age: number;
email: string;
}
const query = QueryBuilder
.initial<User>()
.where('age', '>', 18)
.select('name', 'email')
.exec(); // ✅
// 错误:不能重复 exec
// .exec(); // ❌ 类型错误,exec 后状态为 done关键技术:
- 泛型
T:数据结构类型 Stage:表示构建阶段的联合类型- 条件转移:
.where()后阶段变为'where' - 链式调用返回新实例(或 this)
目标四:利用 const 上下文提升类型精度
问题:对象字面量的类型太宽
ts
const config = {
method: 'POST',
url: '/api/users',
timeout: 5000
};
// config.method: string ← 太宽!无法用于字面量比较解决方案:使用 as const
ts
const config = {
method: 'POST',
url: '/api/users',
timeout: 5000
} as const;
// config.method: 'POST'
// config: { readonly method: "POST"; readonly url: "..."; ... }现在可以安全用于类型判断:
ts
if (config.method === 'GET') { /* ... */ } // ✅ 类型安全结合函数参数使用
text
function request<T extends { method: string }>(config: T) {
type Method = T['method'];
if (Method extends 'GET') {
// 处理 GET
}
}
request({
method: 'GET',
url: '/api'
} as const); // ✅ 精确推断 method 为 'GET'总结:本节核心要点
| 技术 | 用途 | 关键代码 |
|---|---|---|
| 字面量类型 | 限制参数取值 | 'light' | 'dark' |
| 函数重载 | 支持多态调用 | 多个 function fn(...) 签名 |
| Fluent API | 类型安全链式调用 | 泛型 + 阶段状态机 |
as const | 提升类型精度 | { a: 1 } as const → a: 1 |
类型安全的 API 设计目标:让正确用法简单,错误用法报错。
练习题
练习题 1:字面量类型约束
定义一个 setLogLevel 函数,只接受 'debug' | 'info' | 'warn' | 'error':
ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
function setLogLevel(level: LogLevel) {
console.log(`Log level set to ${level}`);
}
setLogLevel('info'); // ✅
setLogLevel('trace'); // ❌ 编译错误练习题 2:函数重载实现
为 parseJSON 实现重载,支持字符串和默认值:
ts
function parseJSON(input: string): any;
function parseJSON<T>(input: string, defaultValue: T): T;
function parseJSON(input: string, defaultValue?: any) {
try {
return JSON.parse(input);
} catch {
return defaultValue;
}
}
parseJSON('{"a":1}'); // any
parseJSON('{"a":1}', {a: 0}); // {a: number}
parseJSON('invalid', 'error'); // string练习题 3:Fluent API 设计
实现一个 Validator 类,支持链式校验:
ts
class Validator<T, Stage extends 'start' | 'checked' | 'ended'> {
private constructor() {}
static start<T>(value: T) {
return new Validator<T, 'start'>() as any;
}
required(): Validator<T, 'checked'> {
// 校验非 null/undefined
return this as any;
}
minLength(n: number): Validator<string, 'checked'> {
return this as any;
}
end(): asserts this is Validator<T, 'ended'> {
// 结束校验
}
}练习题 4:as const 提升精度
以下类型中,method 的类型是什么?
ts
const config1 = { method: 'GET' };
// method: string
const config2 = { method: 'GET' } as const;
// method: 'GET'练习题 5:组合使用
设计一个类型安全的事件系统:
ts
type EventMap = {
click: { x: number; y: number };
keydown: { key: string };
};
function on<E extends keyof EventMap>(
event: E,
handler: (data: EventMap[E]) => void
) {
// ...
}
on('click', (data) => {
data.x; // ✅ number
data.y; // ✅ number
});
on('keydown', (data) => {
data.key; // ✅ string
});通过本节学习,你已经掌握了设计类型安全、防错、易用的 API 的核心方法。这些模式广泛应用于库开发、DSL 设计、配置系统等场景,是高级 TypeScript 工程师的必备技能。