TypeScript实战:泛型工具类型的高级用法与典型场景

TypeScript的泛型工具类型是类型编程的核心能力。BeyondPartialRequired这些内置工具类型,实际项目中大量场景需要自定义工具类型来保证类型安全。这篇文章从需求出发,逐步实现常用的自定义工具类型并给出应用场景。

内置工具类型的盲区

TypeScript内置了Partial、Required、Readonly、Pick、Omit、Record等工具类型,但它们有局限:

  • Partial<T>只做一层浅Partial,嵌套对象不会递归处理
  • Omit<T, K>对联合类型的Key无法做条件分发
  • 没有内置的DeepPartial、DeepReadonly、ValueOf等

这些场景在实际业务中频繁出现,需要自定义。

递归工具类型:DeepPartial与DeepReadonly

表单场景经常需要DeepPartial——一个嵌套3层的配置对象,每一层都可选:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepPartial<T[P]>
    : T[P];
};

// 使用示例
interface AppConfig {
  database: {
    host: string;
    port: number;
    pool: {
      min: number;
      max: number;
    };
  };
  cache: {
    ttl: number;
    prefix: string;
  };
}

// 所有嵌套属性都变为可选
type PartialConfig = DeepPartial<AppConfig>;

// 合法
const config: PartialConfig = {
  database: {
    pool: { max: 10 }
  }
};

DeepReadonly用于冻结配置对象,防止运行时意外修改:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P];
};

条件类型:提取与变换

ExtractRouteParams从路由字符串中提取参数类型:

type ExtractRouteParams<T extends string> = 
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

// /users/:id/posts/:postId → { id: string; postId: string }
type Params = ExtractRouteParams<"/users/:id/posts/:postId">;

function navigate<T extends string>(path: T, params: ExtractRouteParams<T>) {
  // 类型安全的路由跳转
}
navigate("/users/:id/posts/:postId", { id: "1", postId: "42" }); // OK
navigate("/users/:id", { id: "1" }); // OK
navigate("/users/:id", {}); // Error: missing id

键值变换工具类型

API返回的字段名和前端使用的命名规范不同时,需要类型层面的映射:

type CamelToSnake<S extends string> = 
  S extends `${infer First}${infer Rest}`
    ? First extends Uppercase<First>
      ? `_${Lowercase<First>}${CamelToSnake<Rest>}`
      : `${First}${CamelToSnake<Rest>}`
    : S;

type SnakeToCamel<S extends string> = 
  S extends `${infer First}_${infer Rest}`
    ? `${First}${Capitalize<SnakeToCamel<Rest>>}`
    : S;

// 整个对象的键名变换
type TransformKeys<T, Transformer> = {
  [K in keyof T as K extends string 
    ? Transformer extends (s: K) => string 
      ? ReturnType<Transformer> 
      : K 
    : K]: T[K];
};

// 实际应用:API响应转驼峰
type SnakeToCamelKeys<T> = {
  [K in keyof T as K extends string ? SnakeToCamel<K> : K]: 
    T[K] extends object ? SnakeToCamelKeys<T[K]> : T[K];
};

interface ApiUser {
  user_name: string;
  created_at: string;
  email_address: string;
}

type FrontendUser = SnakeToCamelKeys<ApiUser>;
// { userName: string; createdAt: string; emailAddress: string }

类型安全的事件系统

一个常见需求:EventEmitter的类型安全。用映射类型约束事件名和payload:

interface EventMap {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "page:view": { path: string; referrer?: string };
}

class TypedEventEmitter<T extends Record<string, unknown>> {
  private listeners = new Map<string, Function[]>();

  on<K extends keyof T & string>(event: K, fn: (payload: T[K]) => void) {
    const existing = this.listeners.get(event) || [];
    this.listeners.set(event, [...existing, fn]);
  }

  emit<K extends keyof T & string>(event: K, payload: T[K]) {
    (this.listeners.get(event) || []).forEach(fn => fn(payload));
  }
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on("user:login", (payload) => {
  // payload自动推断为 { userId: string; timestamp: number }
  console.log(payload.userId, payload.timestamp);
});

emitter.emit("user:login", { userId: "1", timestamp: Date.now() }); // OK
emitter.emit("user:login", { userId: "1" }); // Error: missing timestamp

运行时类型校验:TypeBox方案

TypeScript类型只在编译期生效,运行时数据(API响应、用户输入)需要校验。TypeBox同时提供TypeScript类型和JSON Schema:

import { Type, Static } from "@sinclair/typebox";

const UserSchema = Type.Object({
  id: Type.String(),
  name: Type.String({ minLength: 1, maxLength: 100 }),
  email: Type.String({ format: "email" }),
  role: Type.Union([Type.Literal("admin"), Type.Literal("user")]),
});

type User = Static<typeof UserSchema>;
// 等价于 { id: string; name: string; email: string; role: "admin" | "user" }

// 运行时校验
import { Value } from "@sinclair/typebox/value";

const input = JSON.parse(responseBody);
if (Value.Check(UserSchema, input)) {
  // 这里input的类型是User
  saveUser(input);
} else {
  const errors = [...Value.Errors(UserSchema, input)];
  throw new ValidationError(errors);
}

类型调试技巧

复杂类型推断结果看不清时,用Expect断言:

type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends 
  (<T>() => T extends Y ? 1 : 2) ? true : false;

// 测试类型是否正确
type _test = Expect<Equal<SnakeToCamel<"user_name">, "userName">>;
// 如果类型正确,编译通过;否则报类型错误

泛型工具类型的核心不是炫技,而是用类型系统在编译期捕获运行时可能出现的错误。业务代码中每多一个精确的类型约束,线上就少一个潜在的bug。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-shi-zhan-fan-xing-gong-ju-lei-xing-de-gao-ji/

(0)
小编小编
上一篇 13小时前
下一篇 13小时前

相关推荐