一个自动从类型生成验证代码的TypeScript转换器。可以将其视为类似ajv和zod的验证库,但它完全依赖于TypeScript编译器,并按需生成原生JavaScript代码。这带来了许多优势:
以下是一些你可以在playground中尝试的例子:
断言函数参数:
// 特殊的`Assert`类型会被检测并生成验证代码 function greet(name: Assert<string>, age: Assert<number>): string { return `Hello ${name}, you are ${age} years old!`; } // 转译为: function greet(name, age) { if (typeof name !== "string") throw new Error("Expected name to be a string"); if (typeof age !== "number") throw new Error("Expected age to be a number"); return `Hello ${name}, you are ${age} years old!`; }
检查值是否为特定类型:
interface User { name: string; age: Min<13>; } const maybeUser = {name: "GoogleFeud", age: "123"}; // `is`函数转译为验证代码 const isUser = is<User>(maybeUser); // 转译为: const isUser = typeof maybeUser === "object" && maybeUser !== null && typeof maybeUser.name === "string" && typeof maybeUser.age === "number" && maybeUser.age > 13;
模式匹配:
type WithValue = {value: string}; // `createMatch`函数创建一个模式匹配函数 const extractString = createMatch<string>([ (value: string | number) => value.toString(), ({value}: WithValue) => value, () => { throw new Error("Could not extract string."); } ]); //转译为: const extractString = value_1 => { if (typeof value_1 === "string") return value_1.toString(); else if (typeof value_1 === "number") return value_1.toString(); else if (typeof value_1 === "object" && value_1 !== null) { if (typeof value_1.value === "string") { let {value} = value_1; return value; } } throw new Error("Could not extract string."); };
npm i --save-dev ts-runtime-checks
<details>
<summary>与ts-patch一起使用</summary>
npm i --save-dev ts-patch
并在tsconfig.json中添加ts-runtime-checks转换器:
"compilerOptions": { //... 其他选项 "plugins": [ { "transform": "ts-runtime-checks" } ] }
之后,你可以使用tspc
CLI命令来转译你的TypeScript代码。
</details> <details> <summary>与ts-node一起使用</summary>const TsRuntimeChecks = require("ts-runtime-checks").default; options: { getCustomTransformers: program => { before: [TsRuntimeChecks(program)]; }; }
要在ts-node中使用转换器,你需要在tsconfig.json
中更改编译器:
npm i --save-dev ts-patch
</details>"ts-node": { "compiler": "ts-patch" }, "compilerOptions": { "plugins": [ { "transform": "ts-runtime-checks" } ] }
ts-runtime-checks
深入解析标记是被转换器检测到的TypeScript类型别名。这些类型并不代表实际的值,而是告诉转换器生成什么代码。可以将它们视为函数!
迄今为止最重要的标记是Assert<T>
,它告诉转译器验证类型T
。还有一些实用
标记,可以在Assert
标记内部使用,以某种方式自定义验证或添加额外的检查。以下是所有实用标记的列表:
Check<Condition, Error, Id, Value>
- 检查Condition
对该值是否为真。NoCheck<Type>
- 不为提供的类型生成检查。ExactProps<Obj, removeExtra, useDeleteOperator>
- 确保值没有任何多余的属性。Expr<string>
- 将字符串转换为表达式。可用于需要JavaScript值的标记中。Infer<Type>
/ Resolve<Type>
- 为类型参数创建验证。该库还导出了一组内置的Check
类型别名,可以用于现有类型以添加额外的检查:
Min<Size>
/ Max<Size>
- 检查数字是否在范围内。Int
/ Float
- 将数字限制为整数/浮点数。Matches<Regex>
- 检查值是否匹配一个模式。MaxLen<Size>
/ MinLen<Size>
/ Length<Size>
- 用于任何具有length
属性的东西,检查其是否在范围内。Eq
- 将值与提供的表达式进行比较。Not
- 对Check
取反。Assert<Type, Action>
Assert
标记通过添加在运行时执行的验证代码来断言一个值是提供的类型。如果值不匹配类型,代码将根据Action
的不同返回一个值或抛出一个错误:
123
、"hello"
、undefined
、true
、false
)- 将返回该字面量。Expr<Type>
- 将返回该表达式。ErrorMsg<rawErrors>
- 将返回错误消息。ThrowError<ErrorType, rawErrors>
- 将抛出ErrorType
类型的错误。如果rawErrors
为true,转换器将传递/返回一个如下所示的对象,而不是错误字符串:
{ // 导致错误的项的值 value: any; // 值的名称 valueName: string; // 预期类型的信息 expectedType: TypeData; }
默认情况下,ThrowError<Error>
被传递给Assert
。
function onMessage(msg: Assert<string>, content: Assert<string, false>, timestamp: Assert<number, ThrowError<RangeError, true>>) { // ... } function onMessage(msg, content, timestamp) { if (typeof msg !== "string") throw new Error("Expected msg to be a string"); if (typeof content !== "string") return false; if (typeof timestamp !== "number") throw new RangeError({value: timestamp, valueName: "timestamp", expectedType: {kind: 0}}); }
Check<Condition, Error, ID, Value>
通过提供包含JavaScript代码的字符串或函数引用,允许你创建自定义条件。
$self
变量获取当前正在验证的值。$parent
函数获取值的父对象。你可以传递一个数字来获取嵌套的父对象。Error
是一个自定义错误字符串消息,在检查失败时会显示。
type StartsWith<T extends string> = Check<`$self.startsWith("${T}")`, `to start with "${T}"`, "startsWith", T>; function test(a: Assert<string & StartsWith<"a">>) { return true; } // 转译为: function test(a) { if (typeof a !== "string" || !a.startsWith("a")) throw new Error('Expected a to be a string, to start with "a"'); return true; }
你可以使用&
(交集)操作符组合检查:
// MaxLen和MinLen是库中包含的类型 function test(a: Assert<string & StartsWith<"a"> & MaxLen<36> & MinLen<3>>) { return true; } // 转译为: function test(a) { if (typeof a !== "string" || !a.startsWith("a") || a.length > 36 || a.length < 3) throw new Error('Expected a to be a string, to start with "a", to have a length less than 36, to have a length greater than 3'); return true; }
当你想接收原始错误时,ID
和Value
类型参数会被使用。如果你不使用原始错误,就不需要使用它们。它们被传递给expectedType
对象,其中ID
是键,Value
是值:
function test(a: Assert<string & StartsWith<"a">, ThrowError<Error, true>>) { return 1; } // 转译为: function test(a) { if (typeof a !== "string" || !a.startsWith(a)) throw new Error({value: a, valueName: "a", expectedType: {kind: 1, startsWith: "a"}}); return 1; }
NoCheck<Type>
跳过对值的验证。
interface UserRequest { name: string; id: string; child: NoCheck<UserRequest>; } function test(req: Assert<UserRequest>) { // 你的代码... } // 转译为: function test(req) { if (typeof req !== "object" || req === null) throw new Error("Expected req to be an object"); if (typeof req.name !== "string") throw new Error("Expected req.name to be a string"); if (typeof req.id !== "string") throw new Error("Expected req.id to be a string"); }
ExactProps<Type, removeExtra, useDeleteOperator>
检查对象是否有任何"多余"的属性(不在类型上但在对象上的属性)。
如果removeExtra
为true,那么不会抛出错误,而是会将任何多余的属性原地从对象中删除。
如果 useDeleteOperator
为 true,则将使用 delete
运算符删除属性,否则该属性将被设置为 undefined。
function test(req: unknown) { return req as Assert<ExactProps<{a: string; b: number; c: [string, number]}>>; } // 转译为: function test(req) { if (typeof req !== "object" || req === null) throw new Error("预期 req 是一个对象"); if (typeof req.a !== "string") throw new Error("预期 req.a 是一个字符串"); if (typeof req.b !== "number") throw new Error("预期 req.b 是一个数字"); if (!Array.isArray(req.c)) throw new Error("预期 req.c 是一个数组"); if (typeof req.c[0] !== "string") throw new Error("预期 req.c[0] 是一个字符串"); if (typeof req.c[1] !== "number") throw new Error("预期 req.c[1] 是一个数字"); for (let p_1 in req) { if (p_1 !== "a" && p_1 !== "b" && p_1 !== "c") throw new Error("属性 req." + p_1 + " 是多余的"); } return req; }
Infer<Type>
你可以在类型参数上使用这个实用类型 - 转换器会遍历该类型参数所属函数的所有调用位置,找出实际使用的类型,创建所有可能类型的联合,并在函数体内进行验证。
export function test<T>(body: Assert<Infer<T>>) { return true; } // 在 fileA.ts 中 test(123); // 在 FileB.ts 中 test([1, 2, 3]); // 转译为: function test(body) { if (typeof body !== "number") if (!Array.isArray(body)) throw new Error("预期 body 是 number 或 number[] 中的一种"); else { for (let i_1 = 0; i_1 < len_1; i_1++) { if (typeof body[i_1] !== "number") throw new Error("预期 body[" + i_1 + "] 是一个数字"); } } return true; }
Resolve<Type>
将类型参数传递给 Resolve<Type>
以将验证逻辑移至调用站点,在那里类型参数被解析为实际类型。
目前,这个标记有一些限制:
Assert
标记中使用(所以你不能在 check
或 is
中使用)as
断言)function validateBody<T>(data: Assert<{body: Resolve<T>}>) { return data.body; } const validatedBody = validateBody<{ name: string; other: boolean; }>({body: JSON.parse(process.argv[2])}); // 转译为: function validateBody(data) { return data.body; } const receivedBody = JSON.parse(process.argv[2]); const validatedBody = (() => { const data = {body: receivedBody}; if (typeof data.body !== "object" && data.body !== null) throw new Error("预期 data.body 是一个对象"); if (typeof data.body.name !== "string") throw new Error("预期 data.body.name 是一个字符串"); if (typeof data.body.other !== "boolean") throw new Error("预期 data.body.other 是一个布尔值"); return validateBody(data); })();
你也可以使用 Transform
标记在你的类型中描述转换。它接受一个函数引用、包含 JavaScript 代码的字符串,或两者的组合:
const timestampToDate = (ts: number) => new Date(ts); const incrementAge = (age: number) => age + 1; type User = { username: string; createdAt: Transform<typeof timestampToDate>; age: Transform<["+$self", typeof incrementAge], string>; };
推荐使用函数引用,因为所有类型都会为你推断。在上面的例子中,我们能够告诉 TypeScript createdAt
在转换为 Date
之前是 number
类型。然而,在 age
中,我们必须指定初始类型(string
),因为第一个转换是一个代码字符串。
一旦你有了可以转换的类型,你可以使用 transform
实用函数来实际执行转换:
const myUser: User = { username: "GoogleFeud", createdAt: 1716657364400, age: "123" } console.log(transform<User>(myUser)) // 转译为: let result_1; result_1 = {}; result_1.createdAt = timestampToDate(myUser.createdAt); result_1.age = incrementAge(+myUser.age); result_1.username = myUser.username; console.log(result_1);
transform
函数的第二个类型参数是一个 Action
,如果提供了它,类型将在转换之前进行验证。查看 Assert
部分了解所有可能的操作。
你也可以通过联合类型执行条件转换:
interface ConditionalTransform { // "age" 可以是数字或字符串 age: number | Transform<typeof stringToNum>, // "id" 可以是字符串或大于3的数字 id: Transform<typeof stringToNum> | Min<3> & Transform<"$self + 1"> } transform<ConditionalTransform, ThrowError>({ age: "3", id: 12 }) // 转译为: let result_1; result_1 = {}; if (typeof value_1.id === "string") { result_1.id = stringToNum(value_1.id); } else if (typeof value_1.id === "number" && value_1.id >= 3) { result_1.id = value_1.id + 1; } else throw new Error("期望 value.id 为字符串或数字,且大于3"); if (typeof value_1.age === "string") { result_1.age = stringToNum(value_1.age); } else if (typeof value_1.age === "number") { result_1.age = value_1.age; } else throw new Error("期望 value.age 为字符串或数字");
你还可以使用 PostCheck
类型在值被转换后执行检查!查看 PostCheck
示例和其他非常复杂的条件转换在这个单元测试中
as
断言你可以使用 as
类型断言来验证表达式中的值。转换器会记住哪些是安全可用的,因此你不会生成重复的验证代码。
interface Args { name: string; path: string; output: string; clusters?: number; } const args = JSON.parse(process.argv[2] as Assert<string>) as Assert<Args>; // 转译为: if (typeof process.argv[2] !== "string") throw new Error("期望 process.argv[2] 为字符串"); const value_1 = JSON.parse(process.argv[2]); if (typeof value_1 !== "object" || value_1 === null) throw new Error("期望值为对象"); if (typeof value_1.name !== "string") throw new Error("期望 value.name 为字符串"); if (typeof value_1.path !== "string") throw new Error("期望 value.path 为字符串"); if (typeof value_1.output !== "string") throw new Error("期望 value.output 为字符串"); if (value_1.clusters !== undefined && typeof value_1.clusters !== "number") throw new Error("期望 value.clusters 为数字"); const args = value_1;
is<Type>(value)
对此函数的每次调用都会被替换为一个立即调用的箭头函数,如果值匹配类型则返回 true
,否则返回 false
。
const val = JSON.parse('["Hello", "World"]'); if (is<[string, number]>(val)) { // val 保证是 [string, number] 类型 } // 转译为: const val = JSON.parse('["Hello", "World"]'); if (Array.isArray(val) && typeof val[0] === "string" && typeof val[1] === "number") { // 你的代码 }
check<Type, rawErrors>(value)
对此函数的每次调用都会被替换为一个立即调用的箭头函数,它返回提供的值以及一个错误数组。
如果 rawErrors
为 true,原始错误数据将被推送到数组中,而不是错误字符串。
const [value, errors] = check<[string, number]>(JSON.parse('["Hello", "World"]')); if (errors.length) console.log(errors); // 转译为: const value = JSON.parse('["Hello", "World"]'); const errors = []; if (!Array.isArray(value)) errors.push("期望值为数组"); else { if (typeof value[0] !== "string") errors.push("期望 value[0] 为字符串"); if (typeof value[1] !== "number") errors.push("期望 value[1] 为数字"); } if (errors.length) console.log(errors);
createMatch<ReturnType, InputType>(function[], noDiscriminatedObjAssert)
创建一个匹配函数,该函数基于提供的函数对输入类型执行模式匹配。数组中的每个函数都是一个匹配分支,其中第一个参数的类型是该分支匹配的类型:
// 我们希望匹配函数返回字符串并接受一个数字 const resolver = createMatch<string, number>([ // 匹配 0 或 1 的分支 (_: 0 | 1) => "不多", // 匹配小于 9 的任何数字的分支 (_: Max<9>) => "几个", // 匹配任何尚未被捕获的数字的分支 (_: number) => "很多" ]); // 转译为: const resolver = value_1 => { if (typeof value_1 === "number") { if (value_1 === 1 || value_1 === 0) return "不多"; else if (value_1 < 9) return "几个"; else return "很多"; } };
你也可以通过省略参数或给它 unknown
或 any
类型来设置默认的匹配分支:
const toNumber: (value: unknown) => number = createMatch<number>([ (value: string | boolean) => +value, (value: number) => value, (value: Array<string> | Array<number> | Array<boolean>) => value.map(v => toNumber(v)).reduce((val, acc) => val + acc, 0), (value: unknown) => { throw new Error("意外的值: " + value); } ]); // 转译为: const toNumber = value_1 => { if (typeof value_1 === "boolean") return +value_1; else if (typeof value_1 === "string") return +value_1; else if (typeof value_1 === "number") return value_1; else if (Array.isArray(value_1)) { if (value_1.every(value_2 => typeof value_2 === "string") || value_1.every(value_3 => typeof value_3 === "number") || value_1.every(value_4 => typeof value_4 === "boolean")) return value_1.map(v => toNumber(v)).reduce((val, acc) => val + acc, 0); } throw new Error("意外的值: " + value_1); };
如果discriminatedObjAssert
参数设置为true,那么对于有区分性的对象(具有字面量属性的对象),只会验证字面量属性。如果你已经验证了源对象或者知道它是正确的,可以使用这个选项。
如果一个值是解构的对象或数组,那么只会验证被解构的属性或元素。
function test({ user: { skills: [skill1, skill2, skill3] } }: Assert< { user: { username: string; password: string; skills: [string, string?, string?]; }; }, undefined >) { // 你的代码 } // 转译为: function test({ user: { skills: [skill1, skill2, skill3] } }) { if (typeof skill1 !== "string") return undefined; if (skill2 !== undefined && typeof skill2 !== "string") return undefined; if (skill3 !== undefined && typeof skill3 !== "string") return undefined; }
string
和字符串字面量
typeof value === "string"
或 value === "literal"
number
和数字字面量
typeof value === "number"
或 value === 420
boolean
value === true || value === false
symbol
typeof value === "symbol"
bigint
typeof value === "bigint"
null
value === null
undefined
value === undefined
[a, b, c]
)
Array.isArray(value)
Array<a>
, a[]
)
Array.isArray(value)
for
循环进行检查。{a: b, c: d}
)
typeof value === "object"
value !== null
value instanceof Class
a | b | c
)
Infer
工具类型。Resolve
工具类型。标记可以在类型别名中使用,所以你可以轻松创建常见模式的快捷方式:
组合检查:
// 将所有与数字相关的检查组合成一个类型 type Num<min extends number | undefined = undefined, max extends number | undefined = undefined, typ extends Int | Float | undefined = undefined> = number & (min extends number ? Min<min> : number) & (max extends number ? Max<max> : number) & (typ extends undefined ? number : typ); function verify(n: Assert<Num<2, 10, Int>>) { // ... } // 转译为: function verify(n) { if (typeof n !== "number" || n < 2 || n > 10 || n % 1 !== 0) throw new Error("期望 n 是一个数字,大于2,小于10,且为整数"); }
通过使用jsonSchema
配置选项,转换器允许你将项目中使用的任何类型转换为JSON Schemas:
"compilerOptions": { //... 其他选项 "plugins": [ { "transform": "ts-runtime-checks", "jsonSchema": { "dist": "./schemas" } } ] }
使用上述配置,您项目中的所有类型都将转换为JSON架构并保存在./schemas
目录中,每个类型都存储在不同的文件中。您还可以使用types
选项或typePrefix
选项来筛选类型:
"jsonSchema": { "dist": "./schemas", // 只有特定类型会被转换为架构 "types": ["User", "Member", "Guild"], // 只有名称以特定前缀开头的类型会被转换为架构 "typePrefix": "$" }
ts-runtime-checks
目前由一个人维护。欢迎并感谢任何贡献。如果您有任何问题或想提交拉取请求,请访问https://github.com/GoogleFeud/ts-runtime-checks
字节跳动发布的AI编程神器IDE
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。
全能AI智能助手,随时解答生活与工作的多样问题
问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。
实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。
一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
AI助力,做PPT更简单!
咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。
选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。
专业的AI公文写作平台,公文写作神器
AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。
OpenAI Agents SDK,助力开发者便捷使用 OpenAI 相关功能。
openai-agents-python 是 OpenAI 推出的一款强大 Python SDK,它为开发者提供了与 OpenAI 模型交互的高效工具,支持工具调用、结果处理、追踪等功能,涵盖多种应用场景,如研究助手、财务研究等,能显著提升开发效率,让开发者更轻松地利用 OpenAI 的技术优势。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号