高级 TypeScript 模式:可扩展的类型安全代码

发布: (2026年1月7日 GMT+8 02:34)
11 min read
原文: Dev.to

Source: Dev.to

封面图片:高级 TypeScript 模式:可类型安全的可扩展代码

Sepehr Mohseni

通用约束与推断

带约束的基本泛型

// Constrain a generic to objects that have an `id` property
interface HasId {
  id: string | number;
}

/**
 * Find an element by its `id`.
 * @param items - Array of items that extend `HasId`
 * @param id    - The id to look for (must be the same type as the item's id)
 */
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
  return items.find(item => item.id === id);
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com' },
];

const user = findById(users, 1); // Type: User | undefined

通用工厂函数

/**
 * Simple store implementation with type‑safe state handling.
 * @param initialState - The initial state of the store; its type is inferred.
 */
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<(s: T) => void>();

  return {
    /** Returns the current state */
    getState: () => state,

    /**
     * Merges a partial update into the current state.
     * @param newState - Partial object containing the fields to update.
     */
    setState: (newState: Partial<T>) => {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener(state));
    },

    /**
     * Subscribes a listener that will be called on every state change.
     * @param listener - Function receiving the new state.
     * @returns A function to unsubscribe the listener.
     */
    subscribe: (listener: (state: T) => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const userStore = createStore({
  user: null as User | null,
  isLoading: false,
  error: null as string | null,
});

userStore.setState({ isLoading: true }); // ✅ Type‑safe

提示: TypeScript 可以根据使用情况推断出复杂类型。让编译器为内部实现承担繁重的工作,但在公共 API 中始终添加显式的泛型约束(例如 T extends HasId),以保持其清晰和安全。

条件类型

基本条件类型

// Generic conditional type that maps a key to a response type
type ApiResponse<T> = T extends 'user'
  ? User
  : T extends 'product'
    ? Product
    : never;

// Example payload types
interface User {
  id: number;
  name: string;
}
interface Product {
  id: number;
  price: number;
}

// Usage
type UserResponse    = ApiResponse<'user'>;    // → User
type ProductResponse = ApiResponse<'product'>; // → Product

实际示例 – 解包 Promise 类型

// Simplified version of the built‑in `Awaited` type
type Unwrap<T> = T extends Promise<infer R> ? Unwrap<R> : T;

// Example
type Result = Unwrap<Promise<Promise<string>>>; // → string

分布式条件类型

条件类型会自动在联合类型上进行分布式处理。这使得它们在对联合的每个成员进行过滤或转换时非常方便。

移除 null / undefined

// Remove `null` and `undefined` from a type
type NonNullable<T> = T extends null | undefined ? never : T;

// Example
type Example = NonNullable<string | null | undefined>; // → string

从联合类型中提取特定成员

// Keep only the `string` members of a union
type ExtractStrings<T> = T extends string ? T : never;

type Mixed       = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // → string | 'hello' | 'world'

注意: 因为条件类型 ExtractStrings<T> 会分别应用到 Mixed 的每个成员上,结果是满足 extends string 检查的成员组成的联合。这种 分布式 行为正是 TypeScript 中强大类型层级过滤的关键。

Source:

推断关键字用于类型提取

infer 关键字允许你在条件类型内部捕获一个类型,并在其他地方复用它。下面列出了从类型中提取信息的最常见模式。

// ------------------------------------------------------------
// 1️⃣ 提取函数的参数类型
// ------------------------------------------------------------
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// ------------------------------------------------------------
// 2️⃣ 提取函数的返回类型
// ------------------------------------------------------------
type Return<T> = T extends (...args: any[]) => infer R ? R : never;

// ------------------------------------------------------------
// 3️⃣ 提取数组(或元组)的元素类型
// ------------------------------------------------------------
type ArrayElement<T> = T extends (infer E)[] ? E : never;

// ------------------------------------------------------------
// 4️⃣ 提取 React 组件的 props 类型
// ------------------------------------------------------------
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

📚 实际示例

// 一个简单的异步函数
async function fetchUser(id: number): Promise<User> {
  /* … */
}

// 使用上面定义的辅助类型
type FetchUserParams = Parameters<typeof fetchUser>; // => [number]
type FetchUserReturn = Return<typeof fetchUser>;     // => Promise<User>

辅助类型的作用

辅助类型输入提取的类型
Parameters<T>(...args: infer P) => anyP – 函数参数的元组类型
Return<T>(...args: any[]) => infer RR – 函数的返回类型
ArrayElement<T>(infer E)[]E – 数组中每个元素的类型
ComponentProps<T>React.ComponentType<infer P>P – 组件的 props 类型

这些工具函数是 TypeScript 中许多高级类型模式的基石,例如创建高阶函数、类型安全的包装器或通用组件库。

映射类型

转换对象类型

// Make all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Make all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Pick specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

高级映射类型

// Create getters for all properties
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

/* Example */
type PersonGetters = Getters<Person>;
/* Result:
{
  getName: () => string;
  getAge: () => number;
}
*/

// Filter properties by their value type
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
}

/* Example */
type NumberProps = FilterByType<Mixed, number>;
/* Result:
{
  id: number;
  count: number;
}
*/

注意: 通过将 模板字面量类型映射类型 结合使用,你可以构建强大的类型转换——这对于生成强类型的 API 客户端、表单构建器以及其他元编程场景非常适用。

类型守卫与缩小

自定义类型守卫

// Type predicate function
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).email === "string"
  );
}

// Usage
function processData(data: unknown) {
  if (isUser(data)) {
    // TypeScript knows `data` is `User` here
    console.log(`User ${data.name} (${data.id})`);
  } else {
    console.warn("Not a user");
  }
}

带类型保护的判别联合

interface SuccessResponse {
  status: 'success';
  data: User;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // TypeScript narrows to SuccessResponse
    console.log(response.data.name);
  } else {
    // TypeScript narrows to ErrorResponse
    console.log(response.message);
  }
}

断言函数

/**
 * Asserts that the supplied value is a `User`.
 * Throws an error if the check fails, allowing TypeScript to narrow the type.
 */
function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error('Value is not a User');
  }
}

/** Example usage */
function processUserData(data: unknown) {
  // Narrow `data` to `User` after the assertion
  assertIsUser(data);
  console.log(data.email); // `data` is now known to be a `User`
}

/**
 * Asserts that a value is neither `null` nor `undefined`.
 *
 * @param value   The value to check.
 * @param message Optional custom error message.
 *
 * @throws Error if `value` is `null` or `undefined`.
 */
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message ?? 'Value is not defined');
  }
}

模板字面量类型

原始段落中未提供代码示例。

构建类型安全的 API

// ------------------------------------------------------------
// 1️⃣ Event handler types
// ------------------------------------------------------------

// Allowed event names
type EventName = 'click' | 'focus' | 'blur';

// Utility type that builds the handler name (e.g. "onClick")
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;

// Example usage
type ClickHandler = EventHandler<'click'>; // "onClick"
type FocusHandler = EventHandler<'focus'>; // "onFocus"
type BlurHandler  = EventHandler<'blur'>;  // "onBlur"
// ------------------------------------------------------------
// 2️⃣ Route parameters extraction
// ------------------------------------------------------------

// Route patterns
type Route =
  | '/users/:id'
  | '/posts/:postId/comments/:commentId';

/**
 * Recursively extracts all `:param` placeholders from a route string.
 *
 * @template T - The route string to analyse.
 * @returns A union of the extracted parameter names.
 */
type ExtractParams<T extends string> =
  // Match a segment that contains a parameter followed by more segments
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    // Match the final segment that contains a parameter
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      // No more parameters
      : never;

// Example extractions
type UserRouteParams    = ExtractParams<'/users/:id'>; // "id"
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
//   → "postId" | "commentId"

上面的代码片段演示了如何:

  1. 生成类型安全的事件处理函数名称 using template literal types and Capitalize.
  2. 提取路由参数 from a URL pattern, yielding a union of the placeholder names.

类型安全的 CSS 属性

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

interface Spacing {
  margin: CSSValue;
  padding: CSSValue;
}

const spacing: Spacing = {
  margin: '16px',    // Valid
  padding: '1.5rem', // Valid
  // margin: '16',   // Error: not a valid CSSValue
};

实用类型实践

(保留章节;未提供具体代码。)

构建表单库

以下是一个小型、类型安全的表单配置助手。它根据数据模型的形状推断适当的输入类型,并允许您添加标签、验证和必填标记。

// ------------------------------------------------------------
// 1️⃣  Generic field‑configuration type
// ------------------------------------------------------------
type FieldConfig<T> = {
  [K in keyof T]: {
    /** Input type inferred from the property type */
    type:
      T[K] extends string
        ? 'text' | 'email' | 'password'
        : T[K] extends number
        ? 'number'
        : T[K] extends boolean
        ? 'checkbox'
        : 'text';

    /** Human‑readable label */
    label: string;

    /** Optional “required” flag */
    required?: boolean;

    /** Optional validator – returns an error string or `undefined` */
    validate?: (value: T[K]) => string | undefined;
  };
};

// ------------------------------------------------------------
// 2️⃣  Example data model
// ------------------------------------------------------------
interface UserForm {
  name: string;
  email: string;
  age: number;
  newsletter: boolean;
}

// ------------------------------------------------------------
// 3️⃣  Concrete configuration for `UserForm`
// ------------------------------------------------------------
const userFormConfig: FieldConfig<UserForm> = {
  name: {
    type: 'text',
    label: 'Full Name',
    required: true,
    validate: value => 
  }
}
Back to Blog

相关文章

阅读更多 »

React 组件中的 TypeScript 泛型

介绍:泛型并不是在 React 组件中每天都会使用的东西,但在某些情况下,它们可以让你编写既灵活又类型安全的组件。