TypeScript 使用指南
重要: 公开 TypeScript 示例默认推荐 schema-dsl/pure + s,避免导入后自动安装 String 原型扩展。
当你需要在纯 DSL 字符串、DSL 种子 builder、命名空间 factory 和可选 String 扩展之间做取舍时,请尽早阅读本页。它说明 TypeScript 能推导什么、哪些仍是运行时约束,以及编辑器提示的边界。
1. 快速开始
1.1 安装
1.2 基础用法
import { s, validate } from 'schema-dsl/pure';
// 定义 Schema
const userSchema = s({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-100'
});
// 验证数据
const result = validate(userSchema, {
username: 'testuser',
email: 'test@example.com',
age: 25
});
if (result.valid) {
console.log('验证通过:', result.data);
} else {
console.log('验证失败:', result.errors);
}
2. TypeScript 中的链式调用
2.1 默认不扩展全局 String 类型
默认情况下,TypeScript 不会获得全局 interface String 链式声明。这样可以让导入 schema-dsl/pure 的项目保持原生 trim()、toLowerCase() 等方法的类型稳定。
因此,直接字符串链式调用默认会报类型错误;只有显式导入 schema-dsl/string-types 时才会获得 String 链式提示:
// ❌ 默认 TypeScript 中会报错
const defaultErrorSchema = s({
email: 'email!'.label('邮箱') // 类型错误:Property 'label' does not exist on type 'string'
});
// ✅ 不扩展全局 String 类型时的默认 TypeScript 路径:使用 factory
const defaultBuilderSchema = s({
email: s.email().label('邮箱').require()
});
2.2 推荐编写入口
按编写目标选择三种推荐入口之一:
import { s } from 'schema-dsl/pure';
// ✅ 纯 DSL:配置最短,字面量内部提示有限
const compactSchema = s({
email: 'email!',
username: 'string:3-32!'
});
// ✅ 显式 DSL 种子:紧凑 DSL + builder 提示
const emailField = s('email!').label('邮箱').pattern(/custom/);
const reusableSchema = s({ email: emailField });
// ✅ factory 写法:最完整的 TypeScript 方法发现
const accountEmail = s.email().label('邮箱').pattern(/custom/).require();
const factorySchema = s({ email: accountEmail });
兼容导出 dsl 仍会保留,但新的公开示例统一使用更短的 s 命名空间。从 schema-dsl/pure 导入时,s 支持纯 DSL 字符串、s('...') 和 s.xxx(),不会安装 String 扩展。
如果项目会通过 transformSchemaDsl() 或 schemaDslEsbuildPlugin() 把静态 String 链式调用编译成 builder 调用,可以显式导入类型入口:
import { s } from 'schema-dsl/pure';
import 'schema-dsl/string-types';
const schema = s({
role: 'admin|user|guest'.label('角色'),
email: 'email!'.label('邮箱').require()
});
直接字符串链式不是默认路径。只有当你明确需要最紧凑源码,并且项目已经显式安装编译期或运行时 String 扩展支持时,才建议使用。
好处:
- ✅ 纯 DSL 字符串仍然是最短配置方式。
- ✅
s('...') 保留 DSL 语法,同时在种子之后提供完整 builder 方法提示。
- ✅
s.email() / s.string() / s.number() 提供最完整的 factory 和方法发现。
- ✅
schema-dsl/pure 入口不会安装 String 扩展。
- ✅ transform +
schema-dsl/string-types 路径只在显式导入时提供 String 链式提示。
- ✅ 已知 DSL 字面量仍可通过
InferSchema / InferDslString 获得轻量静态值类型推导。
2.3 工作原理
// s factory 和 s(string) 都返回按公开 IDslBuilder 链式契约声明的 builder
const emailBuilder = s.email().require();
const sameBuilderShape = s('email!').label('邮箱');
// ^? IDslBuilder - 完整公开链式类型
// DslBuilder 支持所有链式方法,并有完整类型提示
emailBuilder.label('邮箱')
// ^? IDE 自动提示所有可用方法
.pattern(/^[a-z]+@[a-z]+\.[a-z]+$/)
.error({ required: '邮箱必填' });
不要把 s('string:3-32!') 理解为完整的 TypeScript 级 DSL parser。它能为返回的 builder 提供完整链式方法提示;字符串字面量本身只通过 InferDslString<'string:3-32!'> 等辅助类型做粗粒度值类型推导,例如推成 string。长度范围、正则、自定义验证器和本地化消息属于运行时 schema 约束,不会变成 TypeScript 的精确值域类型。
3. 类型推导最佳实践
3.1 方式对比
3.2 推荐写法
✅ 方式 1: 纯 DSL 字符串,保持最短配置
import { s } from 'schema-dsl/pure';
const schema = s({
username: 'username:medium!',
email: 'email!',
age: 'number:18-100'
});
优点:
- ✅ 公开文档中最短的编写方式
- ✅ 不安装 String 扩展
- ✅ 适合只需要内置 DSL 约束的字段
✅ 方式 2: 先定义 DSL 种子,再组合
import { s } from 'schema-dsl/pure';
// 定义可复用的字段
const emailField = s('email!')
.label('邮箱地址')
.error({ required: '邮箱必填' });
const usernameField = s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('用户名')
.error({ pattern: '用户名只能包含字母、数字和下划线' });
// 组合使用
const registrationSchema = s({
email: emailField,
username: usernameField,
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('密码')
.error({ pattern: '密码至少 8 位且必须包含字母和数字' })
});
const loginSchema = s({
email: emailField, // 复用
password: s('string!').label('密码')
});
优点:
- ✅ 字段定义可复用
- ✅ 代码更模块化
- ✅ 适合大型项目
✅ 方式 3: 需要最完整方法发现时使用 s 命名空间 factory
import { s } from 'schema-dsl/pure';
const schema = s({
username: s.string().min(3).max(32).require()
.pattern(/^[a-zA-Z0-9_]+$/)
.label('用户名'),
email: s.email().label('邮箱地址').require(),
age: s.number().min(18).max(100).label('年龄')
});
优点:
- ✅ 完整的 builder 链式方法提示
- ✅ IDE 自动提示所有 factory 与 builder 方法
- ✅ 适合更偏好可发现 API、而不是紧凑 DSL 字面量的用户
❌ 避免未配置或不一致的写法
// ❌ 未导入 schema-dsl/string-types 就直接使用字符串链式调用
const schema = s({
email: 'email!'.label('邮箱') // 可能无类型提示
});
// ❌ 未显式选择 compat/register-string 运行时入口就使用直接 String 链式
// v3 的 root 与 pure 导入都无副作用。
4. 完整示例
4.1 用户注册表单
import { s, validateAsync, ValidationError } from 'schema-dsl/pure';
// 定义 Schema
const registrationSchema = s({
profile: s({
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('用户名')
.error({ pattern: '只能包含字母、数字和下划线' }),
email: s('email!')
.label('邮箱地址')
.error({ required: '邮箱必填' }),
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('密码')
.error({ pattern: '密码至少 8 位且必须包含字母和数字' }),
age: s('number:18-100')
.label('年龄')
}),
settings: s({
emailNotify: s('boolean')
.default(true)
.label('邮件通知'),
language: s('string')
.default('zh-CN')
.label('语言设置')
})
});
// 异步验证(推荐)
async function registerUser(data: any) {
try {
const validData = await validateAsync(registrationSchema, data);
console.log('注册成功:', validData);
return validData;
} catch (error) {
if (error instanceof ValidationError) {
console.log('验证失败:');
error.errors.forEach(err => {
console.log(` - ${err.path}: ${err.message}`);
});
throw error;
}
throw error;
}
}
// 使用
registerUser({
profile: {
username: 'testuser',
email: 'test@example.com',
password: 'StrongPass123!',
age: 25
},
settings: {
emailNotify: true,
language: 'en-US'
}
});
4.2 API 请求验证
import { ValidationError, s, validateAsync } from 'schema-dsl/pure';
import express from 'express';
const app = express();
app.use(express.json());
// 定义 API Schema
const createUserSchema = s({
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('用户名'),
email: s('email!').label('邮箱'),
role: s('string')
.default('user')
.label('角色')
});
// 使用中间件
app.post('/api/users', async (req, res) => {
try {
const validData = await validateAsync(createUserSchema, req.body);
// 创建用户逻辑
const user = await createUser(validData);
res.json({ success: true, data: user });
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({
success: false,
errors: error.errors.map(e => ({
field: e.path,
message: e.message
}))
});
} else {
res.status(500).json({ success: false, message: '服务器错误' });
}
}
});
4.3 表单字段复用
import { s } from 'schema-dsl/pure';
// 定义常用字段
const commonFields = {
email: s('email!')
.label('邮箱地址')
.error({ required: '邮箱必填' }),
username: s('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.label('用户名')
.error({ pattern: '用户名只能包含字母、数字和下划线' }),
password: s('string:8-64!')
.pattern(/^(?=.*[A-Za-z])(?=.*\d).{8,}$/)
.label('密码')
.error({ pattern: '密码至少 8 位且必须包含字母和数字' })
};
// 注册表单
const registrationSchema = s({
...commonFields,
confirmPassword: s('string!')
.label('确认密码')
});
// 登录表单
const loginSchema = s({
email: commonFields.email,
password: s('string!').label('密码') // 登录时不需要强密码验证
});
// 密码重置表单
const resetPasswordSchema = s({
email: commonFields.email,
newPassword: commonFields.password,
confirmPassword: s('string!').label('确认新密码')
});
5. 常见问题
5.1 为什么 TypeScript 中字符串链式调用没有类型提示?
原因: TypeScript 对全局 String.prototype 扩展的类型推导有限制。
解决: 使用 s('...') 包裹字符串:
// ❌ 可能无提示
'email!'.label('邮箱')
// ✅ 完整提示
s('email!').label('邮箱')
5.2 JavaScript 用户需要改变写法吗?
已有 JavaScript 用户若需要直接 String 链式,必须添加 schema-dsl/compat 或 schema-dsl/register-string;root 与 pure 导入都无副作用:
import { s } from 'schema-dsl/pure';
const schema = s({
email: s('email!').label('邮箱'),
age: 'number:18-100'
});
5.3 如何在严格模式下使用?
在 tsconfig.json 中启用严格模式也没问题:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
只需使用 s('...') 即可:
const schema = s({
email: s('email!').label('邮箱') // ✅ 严格模式下正常
});
5.4 如何获取验证后的数据类型?
使用泛型参数:
interface User {
username: string;
email: string;
age?: number;
}
// 同步验证
const result = validate<User>(userSchema, data);
if (result.valid) {
const user: User = result.data; // ✅ 类型安全
}
// 异步验证
const validUser = await validateAsync<User>(userSchema, data);
// ^? User - 完整的类型推导
5.5 如何处理嵌套对象的验证错误?
try {
await validateAsync(schema, data);
} catch (error) {
if (error instanceof ValidationError) {
// 方式 1: 遍历所有错误
error.errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
// 输出: profile.username: 用户名至少3个字符
});
// 方式 2: 获取特定字段错误
const usernameError = error.getFieldError('profile.username');
if (usernameError) {
console.log(usernameError.message);
}
// 方式 3: 获取所有字段错误映射
const fieldErrors = error.getFieldErrors();
// { 'profile.username': {...}, 'profile.email': {...} }
}
}
6. 进阶技巧
6.1 额外业务规则
const schema = s({
username: s('string:3-32!').label('用户名')
});
const result = await validateAsync(schema, data);
if (result.username === 'admin') {
throw new Error('用户名已存在');
}
这种写法的好处是:结构校验仍由 schema-dsl 负责,业务唯一性、数据库查重等规则继续留在 TypeScript 业务层,避免把外部依赖塞进字段声明。
6.2 条件验证
const schema = s({
userType: s('string!').label('用户类型'),
// 使用 s.match() 根据 userType 字段动态验证
companyName: s.match('userType', {
'company': 'string!', // 企业用户必填
'_default': 'string' // 个人用户可选
})
});
6.3 Schema 复用和扩展
import { SchemaUtils, s } from 'schema-dsl/pure';
// 基础用户 Schema
const baseUserSchema = s({
username: s('string:3-32!').label('用户名'),
email: s('email!').label('邮箱')
});
// 扩展为管理员 Schema
const adminSchema = SchemaUtils.extend(baseUserSchema, {
role: s('string!').default('admin').label('角色'),
permissions: s('array<string>').label('权限列表')
});
// 只选择部分字段
const publicUserSchema = SchemaUtils.pick(
baseUserSchema,
['username']
);
7. 性能优化
7.1 复用 Schema 与默认缓存
const schema = s({
email: s('email!').label('邮箱')
});
// 多次验证会复用默认 Validator 的编译缓存
await validateAsync(schema, data1);
await validateAsync(schema, data2);
await validateAsync(schema, data3);
7.2 缓存配置
import { s } from 'schema-dsl/pure';
// 配置缓存大小
s.config({
cache: {
maxSize: 5000, // 缓存条目数
ttl: 60000 // 过期时间(毫秒)
}
});
8. 最佳实践总结
- ✅ 按目标选择入口:
s({}) 中的纯 DSL、s('...') 或 s.xxx()
- ✅ 使用
validateAsync 进行异步验证
- ✅ 为验证结果添加泛型类型参数
- ✅ 复用常用字段定义
- ✅ 使用
ValidationError 类型守卫处理错误
- ✅ 为用户提供友好的错误消息
- ✅ 复用常用 Schema 对象,让默认缓存命中
9. 相关资源
对应示例文件
示例入口: typescript-guide.ts
说明: 展示 TypeScript 下推荐的三种编写入口、validate<T>() / validateAsync<T>()、以及 ValidationError 的字段错误读取方式。