TypeScript 类型系统实战:从基础到高级工具类型

前端工程

为什么工具站需要 TypeScript?

工具库有 200+ 工具页、4 语言 i18n、复杂的工具客户端配置。纯 JavaScript 在以下场景容易出错:

  • 工具配置对象的字段拼写错误
  • i18n key 不存在但未报错
  • API 响应结构与预期不符
  • 重构时遗漏更新引用

TypeScript 在编译期捕获这些错误,而非运行时。


核心类型技巧

1. 类型推断 vs 显式标注

// ✅ 让 TS 推断(简洁)
const tools = [{ href: '/pdf/merge', name: 'PDF合并' }] as const;

// ✅ 需要约束时显式标注
function getTool(href: string): ToolItem | undefined {
  return ALL_TOOLS.find(t => t.href === href);
}

2. 泛型约束

function encode<T extends string | ArrayBuffer>(
  input: T
): T extends string ? string : ArrayBuffer {
  // 根据输入类型返回不同结果
}

3. 联合类型与类型守卫

type ProcessResult =
  | { status: 'success'; data: Blob }
  | { status: 'error'; message: string };

function handleResult(result: ProcessResult) {
  if (result.status === 'success') {
    downloadBlob(result.data); // TS 知道 data 存在
  }
}

高级工具类型

Partial、Required、Pick、Omit

type ToolConfig = {
  href: string;
  name: string;
  blurb: string;
  i18nKey?: string;
};

type ToolUpdate = Partial<ToolConfig>;        // 所有字段可选
type ToolRequired = Required<ToolConfig>;      // 所有字段必填
type ToolPreview = Pick<ToolConfig, 'name' | 'blurb'>; // 只取部分

条件类型

type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>;  // true
type B = IsString<42>;       // false

映射类型

type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };

JSON → TypeScript 生成

工具库 JSON 转 TypeScript 工具的实现逻辑:

function jsonToTypeScript(json: unknown, name = 'Root'): string {
  if (Array.isArray(json)) {
    return `${name}[]`;
  }
  if (typeof json === 'object' && json !== null) {
    const fields = Object.entries(json).map(([key, value]) => {
      const type = jsonToTypeScript(value, capitalize(key));
      return `  ${key}: ${type};`;
    });
    return `interface ${name} {\n${fields.join('\n')}\n}`;
  }
  return typeof json; // string | number | boolean
}

输入 API 响应 JSON,一键生成 TypeScript interface——减少手动编写类型的工作量。


实用模式

satisfies 运算符(TS 4.9+)

const config = {
  locales: ['zh-CN', 'en'],
  defaultLocale: 'zh-CN',
} satisfies RoutingConfig; // 保留字面量类型,同时校验结构

const 类型参数(TS 5.0+)

function createTool<const T extends string>(href: T) {
  return { href } as const;
}
const tool = createTool('/pdf/merge'); // href 类型为 '/pdf/merge',非 string

总结

TypeScript 的类型系统不仅是「加类型注解」,更是一套编译期编程语言。掌握泛型、条件类型和工具类型,可以构建自文档化、重构安全的代码库。工具库的 JSON 转 TypeScript 工具则是类型驱动开发的实用起点。


模板字面量类型(TS 4.1+)

模板字面量类型让类型系统能够操作字符串模式:

// 定义 i18n 路由类型
type Locale = 'zh-CN' | 'en' | 'ja' | 'zh-TW';
type ToolRoute = `/${Locale}/image/${string}` | `/${Locale}/pdf/${string}` | `/${Locale}/encode/${string}`;

// 所有工具路由在编译期校验
const validRoute: ToolRoute = '/en/pdf/merge';     // ✅
const badRoute: ToolRoute = '/invalid/pdf/merge';  // ❌ 类型错误

// 运行时也支持的类型级转换
type CamelCase<S extends string> =
  S extends `${infer P}_${infer R}`
    ? `${P}${Capitalize<CamelCase<R>>}`
    : S;

type Result = CamelCase<'tool_config_key'>; // "toolConfigKey"

工具库用模板字面量类型定义了所有路由参数的类型约束,使路由跳转在 IDE 中即可获得自动补全和拼写校验。


Branded Types(品牌类型)

TypeScript 默认是结构类型系统,不同语义但同结构的类型相互兼容。Branded Types 用编译期标记区分它们:

type Brand<T, B> = T & { __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type FileHash = Brand<string, 'FileHash'>;

function createUserId(id: string): UserId {
  return id as UserId; // 显式转换入口
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const uid = createUserId('usr_123');
getUser(uid);            // ✅
getOrder(uid);           // ❌ 类型错误!UserId 不能作为 OrderId 使用

实用场景

  • 文件处理工具:区分 InputFile vs OutputFile
  • API 响应:区分 RawResponse vs ValidatedResponse
  • 安全性:防止各种 ID 之间混用

Discriminated Unions(可辨识联合)

通过字面量 kind 字段创建穷举完备的类型判断:

// 工具处理结果的联合类型
type ToolResult =
  | { kind: 'pdf_merge'; pages: number; blob: Blob }
  | { kind: 'json_format'; formatted: string; errors: string[] }
  | { kind: 'image_compress'; originalSize: number; compressedSize: number; ratio: number }
  | { kind: 'error'; code: string; message: string };

function renderResult(result: ToolResult) {
  switch (result.kind) {
    case 'pdf_merge':
      return `PDF 合并完成,共 ${result.pages} 页`;
    case 'json_format':
      return result.errors.length
        ? `JSON 格式化完成,发现 ${result.errors.length} 个错误`
        : `JSON 格式化成功`;
    case 'image_compress':
      return `压缩完成:${(result.ratio * 100).toFixed(1)}% (${result.originalSize} → ${result.compressedSize})`;
    case 'error':
      return `处理失败:${result.message}`;
    // ❌ 遗漏任何 case 会触发编译错误(noUnusedLocals + exhaustive check)
  }
}

工具库的 200+ 工具全部使用 Discriminated Union 管理状态,确保每种工具状态都有对应的 UI 渲染逻辑,不可能遗漏。


Async 类型与类型级验证

结合 zod 在编译期和运行时双重验证类型安全:

import { z } from 'zod';

// 一次定义,同时获得 TS 类型 + 运行时校验
const ToolConfigSchema = z.object({
  href: z.string().startsWith('/'),
  name: z.string().min(1).max(50),
  blurb: z.string().max(200),
  category: z.enum(['PDF', 'Image', 'Encode', 'Dev', 'Utils']),
  tags: z.array(z.string()).max(5),
});

type ToolConfig = z.infer<typeof ToolConfigSchema>; // 自动生成 TS 类型

// 运行时安全校验
function loadTools(json: unknown): ToolConfig[] {
  return z.array(ToolConfigSchema).parse(json);
}

本站提供浏览器本地工具,免注册即可试用 →

#TypeScript#类型系统#泛型#工具类型#工程化