当前位置: 首页 > 科技观察

浅谈TS运行时类型检查

时间:2023-03-21 12:51:35 科技观察

What-什么是运行时类型检查?编译时类型检查(静态类型检查):变量类型的静态检查是在编译阶段进行的。编译后的代码不保留任何类型注解信息,对实际代码运行没有影响。运行时类型检查(动态类型检查):在代码实际运行过程中检查数据类型,一般用于在函数参数、返回值等内部和外部约束之间传递数据Why-我们为什么要需要运行时类型检查?TypeScript极大的提升了前端项目的可维护性,也可以帮助我们保证内部编码的类型安全。但是,在与外界传输数据时,仅仅在编译时进行类型检查,难免会出现一些问题。以两次事故为例:内部输入数据:在线接口返回的videoid字段类型由string改为number,前端获取后丢失精度,导致数据输出异常页面上:项目迭代需求中的逻辑变化导致埋点字段缺失,需要花很长时间分析数据才发现是浪费时间。如果我们在运行时做相应的类型检查,把异常上报给监控,问题就可以早点解决,还有其他需求可以想到运行时类型检查的场景:表单场景的类型检查写API的测试/JSB接口上报参数和过滤敏感信息字段可以看出,在涉及到IO数据场景时,需要额外的运行时检查,让数据类型在不符合预期的情况下,及时发现问题。How-如何进行运行时类型检查?interfaceMyDataType{video_id:string;user_info:{user_id:数字;电子邮件:字符串;};image_list:{url:string}[];}constdata:MyDataType=awaitfetchMyData()if(typeofdata.video_id==='string'&&data.user_info&&typeofdata.user_info.user_id==='number'&&typeofdata.user_info.email==='string'&&Array.isArray(data.image_list)&&data.image_list.every((image)=>typeofimage.url==='string')...){//dosomething}如上,我们可以手动写一个运行时类型检查的代码,但是写起来效率低,可维护性差,而且没有使用现有的TS类型,所以我们不得不同时维护两个类型来保证它们之间的同步。下面介绍一下业界的几种类型检查方案。个人认为一个好的方案至少要满足两点:你只需要维护一个类型规则就可以享受静态类型提示和运行时检查。静态和运行时的类型检查能力对等(至少运行时不能比静态检查宽松,否则会出现线上bug)解决方案一-动态静态编写运行时验证规则,并从中提取静态类型JSON形式到通过写JSON描述验证规则,典型的有ajv和tv4,用法如下:importAjv,{JTDDataType}from"ajv/dist/jtd"constajv=newAjv()constschema={properties:{video_id:{type:"string"},user_info:{properties:{user_id:{type:"int32"},email:{type:"string"}}},image_list:{elements:{properties:{url:{type:"string"},}}}}}asconsttypeMyDataType=JTDDataType//typeMyDataType={//video_id:string;//user_info:{//user_id:number;//电子邮件:string;//}&{};//image_list:({//url:string;//}&{})[];//}&{}const数据:MyDataType=awaitfetchMyData()constvalidate=ajv.compile(schema)validate(data)if(validate.errors){//dosomething}优点:JTD支持从已有的schema中提取TS类型,避免维护两个类型定义验证库,会提供一些常用的Advanced验证规则(如日期范围、邮箱格式等)JSON格式易于存储和传输,甚至可以使用其他语言,动态传递验证规则。缺点:Schema格式有额外的学习成本,JSON写起来太复杂冗长枯燥的提示不友好。实现原理:类型检查:根据schema规则遍历比较数据字段提取类型:结合extends、infer、inkeyof、recursion等语法API形式,通过调用API描述组合校验规则。典型的例子包括zod、superstruct、io-ts,用法如下:import{z}from"zod";constschema=z.object({video_id:z.string(),user_info:z.object({user_id:z.number().positive(),email:z.string().email()}),image_list:z.array(z.object({url:z.string()}))});typeMyDataType=z.infer//typeMyDataType={//video_id:string;//user_info:{//user_id:number;//email:string;//};//image_list:{//url:string;//}[];//}constdata:MyDataType=awaitfetchMyData()constparseRes=schema.safeParse(data)if(parseRes.error){//dosomething}优点:通过API组装类型的形状该格式比JSON更灵活,更易于编写。它提供了一些常用的高级验证规则(如日期范围、邮箱格式等)以支持从现有模式中提取TS类型,避免维护两种类型的定义。缺点:有一些额外的学习成本,不能直接用我们掌握的TS语法来描述实现原理:类似于JSON格式,但是实现更轻量(ajv有35k,zod只有10k)方案2-Static+Dynamic主要写静态类型和动态类型一起检查Validation规则是基于类属性装饰器生成的。典型的例子包括class-validator和typeorm。用法如下:import'reflect-metadata'import{plainToClass,Type}from"class-transformer";import{validate,IsString,IsInt,IsEmail,IsObject,IsArray,ValidateNested,}from"class-validator";classUserInfo{@IsInt()user_id:number;@Length(10,20,{message:'名称长度不能小于10,不能大于20'})@IsEmail()email:string;}classLargeImage{@IsString()url:string}classMyData{@IsString()@IsNotEmpty({message:'video_id不能为空'})video_id:string;@IsObject()@ValidateNested()@Type(()=>UserInfo)user_info:UserInfo;@IsArray({message:'数组不能为空'})@ValidateNested({each:true})@Type(()=>LargeImage)image_list:LargeImage[];}constdata:MyData=awaitfetchMyData()constdataAsClassInstance=plainToClass(MyData,data);validate(dataAsClassInstance).then(message=>{//做某事});优点:强制亚原子类型,ORM风格,适合服务端场景提供一些常用的高级验证规则(如日期范围,邮箱格式等)可以自定义验证属性值的错误信息,如@IsArray({message:'Arraycannotbeempty'})缺点:runtimecheck类型和ts类型都要写,但至少写成一块方便。同步校验规则需要声明类,尤其是有嵌套对象的时候,写起来比较麻烦,只能校验类的实例。普通对象需要配合class-transformer转换。实现原理:装饰器+反射(通过装饰器在字段中添加类型规则元数据,通过反射获取这些元数据用于运行时验证)解决方案3-Statictodynamic通过处理TS类型,使用TS类型在运行时自动转换JSONSchema典型例子有typescript-json-schema,用法如下:优点:无需手动维护两个类型定义缺点:不提供检查能力,需要配合额外的验证库。一些TS类型语法不支持转换(例如联合类型)。有些规则需要额外学习它的注解。语法,写起来不方便。实现原理:解析处理TypeScriptASThttps://github.com/YousefED/typescript-json-schema/blob/master/typescript-json-schema.ts编译时从TS类型生成校验码编译时转换TS码转换为具有等效类型检查功能的JS代码。典型的例子包括typescript-is和ts-auto-guard。使用方法如下:配置ts-loader插件:importtypescriptIsTransformerfrom'typescript-is/lib/transform-inline/transformer'...{test:/.ts$/,exclude:/node_modules/,loader:'ts-loader',选项:{getCustomTransformers:程序=>({before:[typescriptIsTransformer(program)]})}}...编译前源代码:import{is}from"typescript-is"interfaceMyDataType{gid:number;user_info:{user_id:数字;电子邮件:字符串;};large_image_list:{url:字符串;}[];}constdata:MyDataType=fetchMyData()constisRightType=is(data)编译产品代码:Object.defineProperty(exports,"__esModule",{value:true});consttypescript_is_1=require("typescript-is");constdata=(0,fetchMyData)();constisRightType=(0,typescript_is_1.is)(data,object=>{function_number(object){;if(typeofobject!=="数字")return{};elsereturnnull;}function_string(object){;if(typeofobject!=="string")return{};elsereturnnull;}function_1(object){;if(typeofobject!=="对象"||对象===null||Array.isArray(object))return{};{if("user_id"inobject){varerror=_number(object["user_id"]);如果(错误)返回错误;}elsereturn{};}{if(对象中的“电子邮件”){varerror=_string(对象[“电子邮件”]);如果(错误)返回错误;}否则返回{};}返回null;}函数_4(对象){;if(typeofobject!=="object"||object===null||Array.isArray(object))返回{};{if(对象中的“url”){varerror=_string(对象[“url”]);如果(错误)返回错误;}否则返回{};}返回null;}函数sa__4_ea_4(对象){;如果(!Array.isArray(object))返回{};for(leti=0;i解析语法树->修改语法树->转换)在运行时提取TS类型信息动态检查的典型解决方案是DeepKit,它基本上将TS类型系统带到JS运行时:编译前的源代码:import{是}来自'@deepkit/type'interfaceMyDataType{video_id:string;user_info:{user_id:number;电子邮件:字符串;};图片列表:{网址:字符串;}[];}constdata:MyDataType=awaitfetchMyData()constisRightType=is(data)编译后的产品代码:Object.defineProperty(exports,"__esModule",({value:true}));consttype_1=__webpack_require__(/*!@deepkit/type*/"@deepkit/type");const__ΩMyDataType=['video_id','user_id','email','user_info','url','image_list','P&4!P&4"'4#&4$M4%P&4&MF4'M'];constdata=(0,fetchMyData)();constisRes=(0,type_1.is)(data,undefined,undefined,[()=>__ΩMyDataType,'n!']);console.log('deepkit',isRes);优点:简单易用,无需维护两种类型,提供高级验证能力如邮箱格式编译类型验证规则后生成的运行时代码很小,体积不易扩展缺点:项目比较新,还没有广泛使用,稳定性未知,运行时类型解释器可能是繁重,性能开销未知实现原理:编译时将TypeScript类型信息转换为字节码(Bytecode),运行时完整保留TS类型信息,然后使用解释器来计算运行时的类型信息,我们也可以在运行时使用丰富的API反射类型信息,用于生成Mock数据等场景import{typeOf,ReflectionKind}from'@deepkit/type';typeOf();//{种类:ReflectionKind.string}typeOf();//{种类:ReflectionKind.number}typeOf();//{种类:ReflectionKind.boolean}typeOf();//{kind:ReflectionKind.union,types:[{kind:ReflectionKind.string},{kind:ReflectionKind.number}]}classMyClass{id:number=0;}typeOf();//{kind:ReflectionKind.class,classType:MyClass,types:[//{kind:ReflectionKind.property,name:'id',type:{kind:ReflectionKind.number},default:()=>0}//]}导入{ReflectionClass}来自'@deepkit/type';classMyClass{id:number=0;doIt(arg:string):void{}}constreflection=ReflectionClass.from(MyClass);reflection.getProperty('id').type;//{种类:ReflectionKind.number}reflection.getProperty('id').isOptional();//falsereflection.getPropertyNames():['id'];reflection.getMethod('doIt').getReturnType();//{种类:ReflectionKind.void}reflection.getMethod('doIt').getParameter('arg').type;//{kind:ReflectionKind.string}//也适用于接口interfaceUser{id:number;}constreflection=ReflectionClass.from();总结没有完美的解决方案。综合来说还是用zod这种API形式的验证库比较好。它既成熟又强大,又灵活易用。放眼未来,deepkit看似很有潜力,其实是一套完整的web开发解决方案,validation只是其中的一部分,还有很多其他充分利用runtime类型的特性。之后TypeScript会支持运行时类型检查吗?github上一直有人提出相关问题,甚至有人建了一个专门的请愿页面,但基本不可能,因为设计目标已经明确表示不会添加运行时代码:Addorrelyonrun-timetypeinformation在程序中,或根据类型系统的结果发出不同的代码。相反,鼓励不需要运行时元数据的编程模式。