首页 >web前端 >js教程 >独特的符号:如何使用符号进行类型安全

独特的符号:如何使用符号进行类型安全

Mary-Kate Olsen
Mary-Kate Olsen原创
2025-01-16 10:48:09474浏览

Unique Symbols: How to Use Symbols for Type Safety

如果您花了一些时间使用 React,您可能会偶然发现 React Query 的 queryOptions() 函数。它的实现看起来非常简单:

export function queryOptions(options: unknown) {
  return options
}

然而,真正的魔力在于它的重载函数签名。那么,它有什么特别之处呢?

如果你不确定什么是重载函数,可以查看这篇文章:函数重载:如何处理多个函数签名

类型化数据库查询

受 React Query 方法的启发,我组合了一个辅助函数,可能对 React 之外的人员有用:一种创建类型化查询(例如 SQL 查询)的简单方法。

export declare const queryParamsSymbol: unique symbol;
export declare const queryReturnSymbol: unique symbol;

export type Query<
  TParams extends Record<string, any> = Record<string, any>,
  TReturn extends Record<string, any> | undefined = undefined,
  TStatement extends string = string,
> = {
  statement: TStatement;
  [queryParamsSymbol]: TParams;
  [queryReturnSymbol]: TReturn;
};

export function query<
  TParams extends Record<string, any> = Record<string, any>,
  TReturn extends Record<string, any> | undefined = undefined,
  TStatement extends string = string,
>(statement: TStatement): Query<TParams, TReturn> {
  return { statement: statement } as Query<TParams, TReturn, TStatement>;
}

与 queryOptions() 类似,该函数本身相当乏味:它接受一条 SQL 语句,将其包装在 Query 类型的对象中,然后返回它。

这是一个如何调用它的简单示例:

const getUserById = query<{ id: number }, { name: string; email: string }>(
  'SELECT name, email FROM users WHERE id = $id',
);

注意我们如何将两种类型作为泛型参数传递。第一个是必需的查询参数 — 在本例中为 id。第二个代表查询的返回类型 - 姓名和电子邮件。

在底层,query() 将这两种类型嵌入到返回的对象中,并将它们存储在 queryParamsSymbol 和 queryReturnSymbol 中。这些符号被声明为唯一符号,这意味着它们仅存在于类型空间中,不会出现在转译的 JavaScript 中。

我使用这些符号来临时存储参数和返回类型,并在需要时检索它们。

type InferQueryParams<TQuery> = TQuery extends Query<infer Params, any> ? Params : never;

type UserQueryParams = InferQueryParams<typeof getUserById>;
//        ^? { id: number }

type InferQueryReturn<TQuery> = TQuery extends Query<any, infer Return> ? Return : never;

type UserQueryReturn = InferQueryReturn<typeof getUserById>;
//        ^? { name: string; email: string }

InferQueryParams 和 InferQueryReturn 只是实用程序类型,用于确认我们的参数和返回类型被正确推断。您可能实际上并不需要它们,但它们可以方便地验证我们的方法。

数据库客户端

现在我们知道如何在查询对象中嵌入参数和返回类型,那么我们如何实际运行这些查询呢?让我们看一下一个可以执行类型化查询的简单数据库客户端:

class DatabaseClient {
  async execute<
    TParams extends Record<string, any>, 
    TReturn extends Record<string, any>
  >(
    query: Query<TParams, TReturn>,
    params: TParams,
  ): Promise<Array<TReturn>> {
    // execute the query and return the results
    // ...
    return [];
  }
}

const db = new DatabaseClient();

// Return type and parameters are inferred from the query object
const result = await db.execute(getUserById, { id: 1 });
//                                              ^? { id: number }
//      ^? Array<{ name: string; email: string }>
console.log(result);

在此示例中,我们将类型化的 getUserById 查询对象传递给 db.execute() 方法。由于 Query 类型同时包含参数和返回类型信息,因此 TypeScript 会自动推断它们。我们可以通过将鼠标悬停在结果上轻松确认这一点,TypeScript 还会建议 id 作为参数对象的属性。

TypeScript 游乐场

您可以在此 TypeScript Playground 中找到完整的代码示例。

以上是独特的符号:如何使用符号进行类型安全的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn