在Node环境中使用SQLite时,查询返回的结果通常是普通的JavaScript对象数组,字段类型由SQLite自身决定,并不天然符合TypeScript的类型系统。为了让业务层获得准确的类型检查和编辑器提示,我们需要把原始行记录转换成定义好的TypeScript对象。下面以better-sqlite3为例,说明几种实用的映射方式。

定义表对应的TypeScript接口
第一步是为数据库表建立清晰的类型描述。接口中的字段名应与SQL查询输出的列名保持一致,或明确标注别名映射关系。这样在后续映射函数中,编译器才能校验属性是否存在、类型是否匹配。
例如用户表包含编号、名称和注册时间,我们可以这样声明:
interface UserRow {
id: number;
name: string;
created_at: string;
}
interface User {
id: number;
name: string;
createdAt: Date;
}
这里把数据库中的created_at在领域模型中转换为Date类型,既贴合业务语义,也方便上层直接进行时间处理。接口分离了存储形态与领域形态,是映射层存在的意义。
编写基础行映射函数
最直观的做法是写一个纯函数,接收UserRow并返回User。在函数体内完成字段重命名和类型转换。由于better-sqlite3默认返回的是object,我们将其断言为UserRow后再处理。
下面这段示例代码展示了单行转换逻辑:
import Database from 'better-sqlite3';
const db = new Database('app.db');
function mapUser(row: UserRow): User {
return {
id: row.id,
name: row.name,
createdAt: new Date(row.created_at)
};
}
const raw = db.prepare('SELECT id, name, created_at FROM users WHERE id = ?').get(1) as UserRow;
const user = mapUser(raw);
console.log(user.createdAt instanceof Date);
这种写法简单明了,适合字段较少的表。它的优点是映射逻辑集中、易于单测;缺点是每张表都要写一遍相似代码。当表数量增多时,可以考虑提取通用映射器。
通用映射工具减少重复
如果很多表只是做字段名下划线转驼峰,以及个别字段类型转换,可以抽象一个通用函数,通过配置项描述转换规则。这样新增表时只需提供配置,不必重复写函数体。
下面是一个支持字段名映射和自定义转换的轻量工具:
type MapperConfig<T, R> = {
fieldMap?: Record<keyof T, keyof R>;
transforms?: Partial<Record<keyof R, (value: any) => any>>;
};
function createMapper<T extends object, R extends object>(config: MapperConfig<T, R>) {
return function map(row: T): R {
const result = {} as R;
const fieldMap = config.fieldMap || {};
for (const key of Object.keys(row) as Array<keyof T>) {
const targetKey = (fieldMap[key] || key) as keyof R;
let value = (row as any)[key];
if (config.transforms && config.transforms[targetKey]) {
value = config.transforms[targetKey]!(value);
}
(result as any)[targetKey] = value;
}
return result;
};
}
使用上述工具,用户映射可改写为配置驱动:
const mapUserGeneric = createMapper<UserRow, User>({
fieldMap: { created_at: 'createdAt' },
transforms: { createdAt: (v: string) => new Date(v) }
});
const rows = db.prepare('SELECT id, name, created_at FROM users').all() as UserRow[];
const users = rows.map(mapUserGeneric);
该方式在保持类型安全的同时降低了样板代码量。需要注意配置中的泛型参数顺序,以及transforms里返回类型要与目标接口吻合,否则编译器仍会报错。
处理一对多嵌套关系
实际业务中常遇到主子表查询,比如用户及其订单。SQLite不支持直接返回嵌套对象,通常先用JOIN查出扁平行,再在TypeScript里做分组聚合。映射函数要能识别主表主键并收集子项。
示例:查询用户和他们的订单,组合成带orders数组的对象:
interface OrderRow {
user_id: number;
order_id: number;
amount: number;
}
interface UserWithOrders {
id: number;
name: string;
orders: Array<{ orderId: number; amount: number }>;
}
const joined = db.prepare(`
SELECT u.id, u.name, o.id as order_id, o.amount
FROM users u LEFT JOIN orders o ON u.id = o.user_id
`).all() as Array<UserRow & OrderRow>;
function mapUsersWithOrders(rows: Array<UserRow & OrderRow>): UserWithOrders[] {
const map = new Map<number, UserWithOrders>();
for (const r of rows) {
if (!map.has(r.id)) {
map.set(r.id, { id: r.id, name: r.name, orders: [] });
}
if (r.order_id != null) {
map.get(r.id)!.orders.push({ orderId: r.order_id, amount: r.amount });
}
}
return Array.from(map.values());
}
这种手写聚合比在SQL里拼接JSON更直观,也更容易在TypeScript侧做校验。若数据量大,要注意JOIN后的行数膨胀问题,必要时分两次查询再关联。
批量查询与性能注意
当映射成千上万行记录时,逐行创建新对象会带来一定的内存和CPU开销。如果只是透传数据到前端,且无需类型转换,可直接使用as断言返回;若确实需要领域对象,建议复用映射函数并避免在其中执行额外IO。
使用事务或预编译语句能减少SQLite侧消耗,而映射侧应保持纯计算。下面展示预编译语句配合映射的用法:
const stmt = db.prepare('SELECT id, name, created_at FROM users');
const allUsers = (stmt.all() as UserRow[]).map(mapUserGeneric);
总体来看,把SQLite结果映射为TypeScript类型化对象的核心在于:明确接口边界、集中转换逻辑、按需抽象工具。这样既享受了SQLite的轻量,又保留了TypeScript带来的可靠性和可维护性。
TypeScriptSQLitetype_mapping修改时间:2026-08-02 13:21:30