TypeScript 的模板字面量类型(Template Literal Types)自 4.1 版本引入以来,一直是类型系统中被低估却极其强大的特性。它允许我们在类型层面拼接、推断和约束字符串,从而实现许多以往只能依赖运行时检查才能完成的工作。其中一个非常实用的场景,就是在编译期对 SQL 查询语句进行类型检查,避免字段名拼写错误、表名错误,甚至防止 SQL 注入式的字符串拼接问题。
本文将从模板字面量类型的基础讲起,逐步构建一个能对 SQL 查询语句进行类型约束的简易类型系统,并讨论其在实际项目中的价值与边界。
为什么需要类型检查 SQL?
在 Node.js 或前端项目中,我们经常需要写 SQL 语句。传统的写法是直接写字符串:
const sql = "SELECT id, name FROM users WHERE age > 18";
这样的代码没有任何类型保护。如果表名写错、字段名拼错,或者不小心把用户输入直接拼接到 SQL 中,编译器都不会报错。问题往往要到运行时才暴露,甚至引发安全漏洞。
当然,我们可以使用 ORM 或 Query Builder 来获得类型安全,但有时我们仍然需要手写 SQL,或者维护遗留代码。这时,模板字面量类型就能派上用场:它可以在不引入额外运行时库的情况下,为 SQL 字符串提供一定程度的静态检查。
模板字面量类型基础
模板字面量类型使用反引号语法,结合 ${} 占位符,在类型层面构造新的字符串字面量类型。例如:
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, world"; // OK
const b: Greeting = "Hi, world"; // 错误
它还可以与联合类型结合,生成笛卡尔积:
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Color}-${Size}`;
// "red-small" | "red-large" | "blue-small" | "blue-large"
此外,TypeScript 提供了几个内置的字符串操作类型:Uppercase、Lowercase、Capitalize、Uncapitalize,它们可以配合模板字面量类型实现更复杂的转换。
构建 SQL 表结构类型
要对 SQL 进行类型检查,首先需要定义数据库的表结构。我们可以用接口来描述:
interface DatabaseSchema {
users: {
id: number;
name: string;
age: number;
email: string;
};
orders: {
id: number;
userId: number;
amount: number;
createdAt: string;
};
}
接下来,我们需要一个工具类型,用来判断某个字符串是否是某个表的合法字段名。这可以通过 keyof 和模板字面量类型结合实现。
解析 SELECT 语句
一个简单的 SELECT 语句格式为:
SELECT 字段列表 FROM 表名 [WHERE 条件]
我们希望在类型层面解析这个字符串,并检查字段列表中的每个字段是否属于指定的表。
首先,定义一个工具类型,用于将逗号分隔的字段字符串拆分成联合类型:
type Split<S extends string, Delimiter extends string> =
S extends `${infer Head}${Delimiter}${infer Tail}`
? Head | Split<Tail, Delimiter>
: S;
然后,去除字段名两边的空格:
type Trim<S extends string> =
S extends ` ${infer R}` ? Trim<R> :
S extends `${infer R} ` ? Trim<R> :
S;
现在,我们可以定义一个类型,检查字段列表中的每个字段是否都是表 T 的键:
type ValidFields<
T extends keyof DatabaseSchema,
Fields extends string
> = Split<Fields, ","> extends infer F
? F extends string
? Trim<F> extends keyof DatabaseSchema[T]
? true
: false
: never
: never;
但这个类型只能返回 true 或 false,我们更希望它直接约束整个 SQL 字符串。下面定义一个 SelectQuery 类型:
type SelectQuery<
T extends keyof DatabaseSchema,
Fields extends string
> = `SELECT ${Fields} FROM ${T}`;
然后,我们可以创建一个函数,接受这样的查询字符串,并利用条件类型进行校验:
function query<
T extends keyof DatabaseSchema,
Fields extends string
>(
sql: SelectQuery<T, Fields> & (
ValidFields<T, Fields> extends true ? unknown : never
)
): void {
// 执行查询
}
使用示例:
query("SELECT id, name FROM users"); // OK
query("SELECT id, wrongField FROM users"); // 错误:wrongField 不是 users 的字段
query("SELECT id, name FROM wrongTable"); // 错误:wrongTable 不是表名
这样,编译器就能在编写阶段捕获字段名和表名的错误。
处理 WHERE 子句
WHERE 子句的解析更复杂,但我们可以简化处理:只检查字段名是否合法,不检查操作符和值的类型。可以扩展 SelectQuery 类型,允许可选的 WHERE 部分:
type WhereClause<T extends keyof DatabaseSchema> =
`WHERE ${string}`;
type SelectQueryWithWhere<
T extends keyof DatabaseSchema,
Fields extends string,
Where extends string | undefined = undefined
> = Where extends undefined
? `SELECT ${Fields} FROM ${T}`
: `SELECT ${Fields} FROM ${T} ${Where}`;
然后,在函数签名中,我们可以要求 WHERE 子句中的字段名也必须合法。这需要更复杂的解析,但核心思路是一样的:利用模板字面量类型提取字段名,再用 keyof 进行约束。
防止 SQL 注入
模板字面量类型还可以用来标记“已转义”的字符串。例如,我们可以定义一个 SafeString 类型,只有经过特定函数处理的字符串才能被标记为安全:
type SafeString = string & { __brand: "safe" };
function escape(value: string): SafeString {
// 实际转义逻辑
return value.replace(/'/g, "''") as SafeString;
}
function rawQuery(sql: SafeString): void {
// 执行查询
}
const userInput = "'; DROP TABLE users; --";
rawQuery(userInput); // 错误:string 不能赋值给 SafeString
rawQuery(escape(userInput)); // OK
虽然这不是模板字面量类型的直接应用,但结合模板字面量类型,我们可以构建更复杂的 SQL 片段类型,确保只有合法的片段才能拼接。
实际应用中的限制
模板字面量类型虽然强大,但在 SQL 类型检查上仍有明显限制:
- 递归深度限制:TypeScript 对递归类型有深度限制,过长的字段列表可能导致编译器报错。
- 性能问题:复杂的模板字面量类型会显著增加编译时间,尤其是在大型项目中。
- 无法覆盖所有 SQL 语法:完整的 SQL 语法非常复杂,类型系统难以完全模拟。我们只能对常见模式进行约束。
- 错误信息不友好:当类型不匹配时,TypeScript 的错误信息可能非常冗长,难以定位问题。
因此,模板字面量类型更适合作为辅助手段,而不是完全替代 ORM 或 Query Builder。它可以在手写 SQL 的场景下提供一层额外的保护,减少低级错误。
总结
TypeScript 的模板字面量类型为字符串操作打开了新的大门。通过巧妙地组合条件类型、映射类型和字符串操作类型,我们可以在编译期对 SQL 查询语句进行一定程度的类型检查,包括表名、字段名的合法性验证。这不仅能提前发现错误,还能提升代码的可维护性。
当然,这种方案并非银弹,它需要权衡编译性能和类型复杂度。但在合适的场景下,它确实是一种优雅且实用的技术。希望本文能为你提供一些灵感,让你在下次手写 SQL 时,多一份类型安全的保障。
未经允许不得转载:任鹏个人博客 » TypeScript 模板字面量类型的妙用:实现 SQL 查询语句的类型检查


朋友圈点赞图在线生成源码