Skip to content

类型安全的 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) { /* ... */ }

结合 enumas 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 consta: 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 工程师的必备技能。