Published on
1030

从零理解 TS 类型编程:以提取数据库列名为例

Authors
  • avatar
    Name
    小辉辉
    Twitter

在编写类型安全的 SQL 查询构建器(Query Builder)或 ORM 框架时,我们经常能看到类似下面这样极其精妙的 TypeScript 类型定义:

export type AnyColumn<DB, TB extends keyof DB> = {
    [T in TB]: keyof DB[T];
}[TB] & string;

这段代码虽然简短,却完美融合了 TypeScript 高级类型编程中的多个核心概念。它的主要作用是:从数据库类型 DB 指定的若干张表(TB)中,提取出所有列名(字段名)的联合类型。为了彻底搞懂它的底层逻辑,我们可以通过一个具体的例子,将其拆解为四个步骤来逐步推导。

前置假设与例子

假设我们有一个描述数据库结构的 DB 接口,以及一个代表表名的联合类型 'users'

interface DB {
  users: {
    id: number;
    name: string;
    email: string;
  };
  posts: {
    title: string;
    content: string;
  };
}

// 假设我们传入的泛型参数如下:
// DB = 上面的 DB 接口
// TB = 'users'

第一步:映射类型遍历表名 { [T in TB]: ... }

代码中的 { [T in TB]: keyof DB[T]; } 使用了映射类型(Mapped Types)。这类似于在 JavaScript 中对一个数组执行 map 操作,或者在对象上执行 for...in 循环。

它会遍历联合类型 TB 中的每一个表名(作为键 T),并生成一个全新的对象类型。因为我们的 TB'users',所以它会生成一个只包含 users 属性的临时对象框架:

// 第一步的中间产物(伪代码)
{
  users: ... // 等待计算
}

注:如果 TB 传入的是 'users' | 'posts',这里就会生成 { users: ...; posts: ... }

第二步:提取列名 keyof DB[T]

接下来看映射类型中的值部分:keyof DB[T]。这里 T 此时代表 'users',那么 DB[T] 就是 DB['users'],也就是 users 表的行结构(即 { id: number; name: string; email: string; })。

keyof 是 TypeScript 的索引类型查询操作符,它会提取出该对象所有键的联合类型。因此,keyof DB['users'] 的结果就是 'id' | 'name' | 'email'

结合第一步,此时整个映射类型的完整结果是:

{
  users: 'id' | 'name' | 'email'
}

第三步:索引访问取联合值 }[TB]

代码紧接着使用了 }[TB],这属于索引访问类型(Indexed Access Types),也叫 Lookup Types。它的作用是从上一步生成的临时对象中,再次根据 TB(即 'users')去提取对应的值。

也就是执行:{ users: 'id' | 'name' | 'email' }['users']。这一步操作会直接“拆包”,取出里面的值,最终得到 'id' | 'name' | 'email'

如果 TB 是多张表的联合(如 'users' | 'posts'),这一步就会取出两张表的所有列名,并自动合并成一个巨大的联合类型(例如 'id' | 'name' | 'email' | 'title' | 'content')。

第四步:约束为字符串 & string

最后,代码使用了 & string。这是**交叉类型(Intersection Types)**的应用,目的是将前面的结果与 string 取交集。

在 JavaScript 中,对象的键除了 string,还可能是 numbersymbol。加上 & string 是一个防御性的编程技巧,它可以过滤掉可能存在的数字或符号类型的键,确保最终得到的列名绝对是字符串类型。

推导结果为:('id' | 'name' | 'email') & string,最终结果依然是 'id' | 'name' | 'email'。这样做能极大避免在后续进行字符串拼接或生成 SQL 语句时出现意外的类型报错。

总结

整个 AnyColumn 类型的执行流程可以概括为:遍历指定的表名 -> 提取每张表里的所有字段名 -> 合并这些字段名为一个联合类型 -> 过滤并确保字段名都是字符串

最终,AnyColumn<DB, 'users'> 的类型就变成了 'id' | 'name' | 'email'。这种写法能够让开发者在编写数据库查询代码时,获得极其精准的字段名自动提示(IntelliSense),并在拼写错误时立刻得到编译器的警告,是构建高健壮性 Node.js 后端项目的利器。