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

以淘宝店铺为例,谈谈 TypeScript ESLint 规则集考量

时间:2023-03-13 06:07:05 科技观察

以淘宝店为例,谈谈TypeScriptESLint规则集的注意事项无论如何,我的看法是稍微正式一点的项目肯定有ESLint,不管是直接使用extends这样的简单推荐配置:['eslint:recommend'],还是仔细研究了一整套适用于我的规则集。Lint工具最大的帮助就是保持了统一的语法。至少项目中所有的JavaScript文件要使用统一的单双引号、分号、缩进等(编辑个人无法保证)。其次,Lint帮助你的代码更简洁有效,比如不允许未使用的变量,在JSX/TSX中使用速记true属性(而不是)等。另外值得一提的是,ESLint并不总是试图简化你的代码,在很多情况下它会要求你写更多的代码来换取提高可读性和安全性,特别是在TypeScript场景下,explicit-module-boundary-types规则会要求你提供函数和类方法显式声明它的返回值,switch-exhaustiveness-check规则将要求您处理联合类型变量的所有类型分支。本文来源于本人团队(淘宝店)制定、实施、推广ESLint规则集的成果。会简单介绍一批我认为在TypeScript分享中非常必要的规则。通过本文,你将了解我们在制定规则时考虑的是什么,思考对TypeScript代码的约束,以及如何在你自己的团队内推广这套规则。另外,淘系技术部的前端架构团队正在淘系内推广AppLint,并计划将ESLint作为CI/CD的刺刀之一推广到整个淘系前端。欢迎群内同学了解和试用。附言我参与的QCon+主题:TypeScript在中大型项目的实施[1]、TypeScript研发法规在淘宝店铺的实施[2]本课程包括我们团队从JavaScript迁移到TypeScript并实现完整的经验研发协议。欢迎收听~基本约束为了适应读者可能有的不同严格约束,规则分为基本约束和严格约束。每个人在所有项目中都使用它,甚至是个人项目——说实话,我写过TypeScript,还关心这个Lint小规则吗?严格约束部分更关注类型以及ECMAScript和TypeScript的特殊语法,适合对代码质量要求较高的同学。这里就不给出推荐的错误级别了,就算都是warn,只要你打开了,至少以后心情好的时候会修复吧?(对吧?)array-typeTypeScript支持使用Array和T[],这个规则管理项目中这两种数组类型的声明。支持的配置:UseonlyoneofArrayorT[]UseT[]forprimitivetypesandtypealiases,useArrayforobjecttypes,functiontypes,etc.(推荐)Why?:对于与此效果完全一致的语法,我们所需要的只是决定一个规范并在任何地方使用该规范。其实这类规则(以及后面的类型断言语法)和单引号/双引号的基本规则类似,是否加分号,如果不能接受上一行代码中的单引号,则没有理由在这里接受一个Array而在那里接受一个number[]。另外,个人比较推荐统一使用[]。await-thenable只允许异步函数、Promise、PromiseLike的await调用为什么:避免无意义的await调用。ban-ts-comment禁止使用@ts-指令,或者在提供说明的情况下允许使用,比如://@ts-expect-error这里的类型太复杂,会加上later//@ts-nocheck未被迁移的文件该规则建议与prefer-ts-expect-error结合使用,详见下文。为什么:如果写any就叫AnyScript,那么写@ts-ignore就可以叫IgnoreScript。ban-types禁止某些值被标记为类型。这条规则可以针对每一种被禁止的类型提供具体的说明,以便在触发这条规则时,在出现错误时给予很好的提示。禁用{}、Function、object等场景类被标记为类型,为什么?使用{}将使您难以前进:{}类型上不存在属性“foo”,因此如果您使用{},您很可能需要返回类型断言或更改以下任何内容,并且使用对象函数根本没有意义。对于未知的对象类型,你应该使用Record对于函数类型,你应该使用带有输入参数和返回值标记的特定类型:typeSomeFunc=(arg1:string)=>void,或者在未知场景下使用typeSomeFunc=(...args:任何[])=>任何。consistent-type-assertionsTypeScript通过as和<>两种不同的语法支持类型断言,例如:constfoo={}asFoo;constfoo={};//同样有常量断言constfoo=[1,2];constfoo=[1,2,3]asconst;该规则限制了统一类型断言语法的使用。我个人一般在Tsx中使用as,其他时候尽量使用<>。原因是<>更简洁。Why:和array-type类似,语法统一,但是需要注意的是在Tsx项目中使用<>断言会报错,因为不像泛型,可以显式的告诉编译器这是一个泛型语法而不是比一个组件。.explicit-module-boundary-types函数和类方法的返回值需要显式指定,而不是依赖类型推导,如:constfoo=():Foo=>{};why:通过显式指定直观区分函数的作用,比如副作用等,显式指定函数的返回值也能在一定程度上提高TypeScriptCompiler的性能。no-extra-non-null-assertion不允许额外的重复非空断言://xfunctionfoo(bar:number|undefined){constbar:number=bar!!!;}Why:um,whynot?prefer-for-of当你使用for循环遍历数组时,如果索引只是用来访问数组成员,那么应该换成for...of。Why:如果不是为了兼容场景,这种场景下真的没有必要使用for循环。prefer-nullish-coalescing&&prefer-optional-chainuse??而不是||和a?.b而不是a&&a.b。为什么:逻辑或||会将0和""视为false并导致错误的应用程序默认值,而可选链接可以带来比逻辑和&&更简洁的语法(特别是当属性访问是更多嵌套层时,或者当值来自函数时,例如document.querySelector),更好的配合??:constfoo=a?.b?.c?.d??'默认';。no-empty-interface不允许定义空接口,单继承下可以配置允许空接口://xinterfaceFoo{}//√interfaceFooextendsBar{}为什么:没有父类型的空接口其实等于{},虽然我不确定你用它做什么,但我可以告诉你这是不对的。但是单继承的空接口场景比较多,比如先确定继承关系,再添加成员。no-explicit-any不允许显式任何。实际上这条规则只设置为warn级别,因为用unknown+typeassertion替换anyorall的代价非常大。建议配合tsconfig的--noImplicitAny(勾选implicitany),尽可能保证类型的完整性和覆盖率。no-inferrable-types不允许不必要的类型注释,但可以配置为允许对类的属性成员和函数的属性成员进行额外的注释。constfoo:string="linbudu";classFoo{prop1:string="linbudu";}functionfoo(a:number=5,b:boolean=true){//...}为什么:对于普通变量,同实际分配一致的类型注解是没有意义的。TypeScript的控制流分析可以很好地做到这一点。对于函数参数和类属性,主要是保证一致性,即函数的所有参数(包括重载的个别声明),一个类的所有属性都有类型注解,而不仅仅是没有初始值的参数/属性。no-non-null-asserted-nullish-coalescing不允许同时进行空合并的非空断言:bar!??tmpwhy:redundancyno-non-null-asserted-optional-chaindisallowsnon-nullassertionwithoptional链选择同时使用:foo?.bar!Why:和前面的规则一样,是多余的,说明你对!????的理解。是不合适的。no-throw-literal不允许直接抛出一个字符串如:throw'err',只抛出Error的实例或基于Error的派生类,如:thrownewError('Oops!')。Why:抛出的Error实例可以自动收集调用栈信息,同时借助proposal-error-cause[3]提案,还可以跨调用栈附加错误原因和传递上下文信息。不过,真的会有人直接丢一个字符String吗??no-unnecessary-type-arguments不允许泛型参数与默认值一致,如:functionfoo(){}foo();为什么:为了代码简单。no-unnecessary-type-assertion不允许与实际值一致的类型断言,例如:constfoo='foo'asstring。为什么:你明白了。no-unnecessary-type-constraint禁止与默认约束一致的泛型约束,例如:interfaceFooAny{}。Why:同样是为了代码的简化,在TS3.9之后,对于未指定的泛型约束,默认使用unknown,在此之前为any。知道了这些以后就不用再写extendsunknown了。.non-nullable-type-assertion-style这个规则要求当类型断言只起到去除空值的作用时,比如string|undefined类型断言是字符串,应该换成非空断言!constfoo:string|undefined="foo";//√foo!;//xfooasstring;Why:当然是因为代码的简化!该规则的本质是检查断言的类型子集是否只去掉了空值部分,所以对于有意义的类型分支无需担心实际的Joint类型误判。prefer-as-const对于常量断言,使用asconst而不是,类似于上面的consistent-type-assertions规则。prefer-literal-enum-member对于枚举成员值,只允许普通字符串、数字、null、正则表达式,不允许变量复制、模板字符串等需要计算的操作。Why:虽然TypeScript允许使用各种合法的表达式作为枚举成员,但是由于枚举编译结果有自己的作用域,可能会导致赋值错误,如:constimOutside=2;constb=2;enumFoo{outer=imOutside,a=1,b=a,c=b,}其中c==Foo.b==Foo.c==1,或c==b==2?观察编译结果:"usestrict";constimOutside=2;constb=2;varFoo;(function(Foo){Foo[(Foo["outer"]=imOutside)]="outer";Foo[(Foo["a"]=1)]="a";Foo[(Foo["b"]=1)]="b";Foo[(Foo["c"]=1)]="c";})(Foo||(Foo={}));知识哥们?prefer-ts-expect-error使用@ts-expect-error而不是@ts-ignore。why:@ts-ignore和@ts-expect-error的区别在于前者是ignore,不管下一行是否真错,直接放弃下一行的类型检查,而后者则期望下一行确实有错误,实际没有错误时会在下一行抛出错误。使用此类干扰代码检查指令应谨慎使用,任何情况下都不应用作逃生舱口(因为它确实比任何一个都好),如果必须使用它,请务必使用它适当.promise-function-async返回Promise的函数必须标记为异步。这个规则确保函数的调用者只需要处理try/catch或rejectedpromises。为什么:你还需要解释吗?严格约束no-unnecessary-boolean-literal-compare不允许布尔类型变量和true/false之间进行===比较,如:declareconstsomeCondition:boolean;if(someCondition===true){}为什么:首先,记得我们写的是TypeScript,所以不要认为你的变量值可能为null,所以需要这样判断。如果它真的发生了,那么你的TS类型注释是错误的。另外,此规则的配置项最多允许boolean|null与true/false进行比较,因此使您的类型更精确。consistent-type-definitionsTypeScript支持通过类型和接口声明对象类型。可以通过这个规则来约束它为统一的声明方式,即只使用其中的一种。为什么:先说说我是怎么做到的:在大多数场景下,使用interface来声明对象类型,type应该用来声明联合类型、函数类型、工具类型等,比如:interfaceIFoo{}typePartial={[PinkeyofT]?:T[P];};typeLiteralBool="true"|"false";有几个原因:通过命名约定规则(可以用来检查接口是否按照规范命名),我们可以看到IFoo立即知道它是一个接口,并且立即知道它是一个类型别名当看到Bar时,配置:{"@typescript-eslint/naming-convention":["error",{"selector":"interface","format":["PascalCase"],"custom":{"regex":"^I[A-Z]","match":true}}]}接口在类型编程中的作用非常有限,只支持extends、generics等简单能力,应该只用于定义某些结构。并且TypeAlias可以使用除extends之外的所有常见映射类型、条件类型和其他编程语法。同时,“类型别名”的含义也意味着你实际用它来对类型(联合类型)、抽象类型(函数类型、类类型)进行分类。method-signature-style声明方法签名有两种方式:方法和属性,区别如下://methodinterfaceT1{func(arg:string):number;}//propertyinterfaceT2{func:(arg:string)=>number;}这个规则约束了声明方式,推荐使用第二种属性方式。Why:首先,这两个方法之所以叫method和property,显然是因为它们对应的写法。method方法类似于在Class中定义方法,而property类似于定义普通的接口属性,只不过它的值是一个函数类型。推荐属性最重要的原因是通过使用属性+函数值定义,作为值的函数类型可以享受更严格的类型检查(`strictFunctionTypes`[4]),这种配置将使用逆变(Contravariance)而不是协变(covariance)检查函数参数。以后我会单独写一篇关于协方差和求逆的文章。这里我就不展开了。如果你有兴趣,可以阅读TypeScriptTypesContravariantcovariance。consistent-type-imports约束使用importtype{}导入类型,如://√importtype{CompilerOptions}from"typescript";//ximport{CompilerOptions}from"typescript";why:importtype可以帮助你更好的在头部组织项目的导入结构(尽管TypeScript4.5支持类型和值的混合导入:import{foo,typeFoo},建议通过拆分值导入和输入导入语句)。值导入和类型导入在TypeScript中使用不同的堆空间进行存储,因此无需担心循环依赖(因此可以从父组件导入子组件,子组件导入父组件中定义的类型)。一个简单、组织良好的导入语句示例:import{useEffect}from"react";import{Button,Dialog}from"ui";import{ChildComp}from"./child";import{store}from"@/store";import{useCookie}from"@/hooks/useCookie";import{SOME_CONSTANTS}from"@/utils/constants";importtype{Foo}from"@/typings/foo";importtype{Shared}from"@/typings/shared";importstylesfrom"./index.module.scss";restrict-template-expressions模板字符串中的计算表达式必须返回一个字符串。这个规则可以配置为允许数字、布尔值、潜在的空值和正则表达式,或者你可以允许任意值,但这很无聊......为什么:模板表达式中的非字符串和数值容易出现潜在问题,如:constarr=[1,2,3];constobj={name:"linbudu"};//'arr:1,2,3'conststr1=`arr:${arr}`;//'obj:[objectObject]'conststr2=`obj:${obj}`;无论哪种情况,都不是你想看到的,因为它实际上是你无法控制的。建议在规则配置中只开启allowNumber允许数字,其他类型不允许。你需要做的应该是在将这个变量填充到模板字符串中时,用实际的逻辑进行转换。当switch-exhaustiveness-checkswitch的判断条件为联合类型时,需要对每个类型分支进行处理。如:typePossibleTypes="linbudu"|"qiongxin"|"developer";letvalue:PossibleTypes;letresult=0;switch(value){case"linbudu":{result=1;break;}case"qiongxin":{result=2;break;}case"developer":{result=3;break;}}Why:经常出现在工程项目中。出现问题的原因是一些功能逻辑点只是口耳相传。我不知道我还错过了什么。例如,联合类型变量中的每个类型分支可能需要特殊的处理逻辑。你也可以通过TypeScript中的nevertype检查实际代码:=="number"){console.log("num!");}elseif(typeofstrOrNumOrBool==="boolean"){console.log("bool!");}else{const_exhaustiveCheck:never=strOrNumOrBool;thrownewError(`Unknowninputtype:${_exhaustiveCheck}`);}这里在编译期和运行期做了双重保证,确保联合类型的新类型分支也需要妥善处理。可以参考开头的never类型文章了解更多never相关的用法。除了union类型,还可以使用never类型来保证每一个枚举成员都需要处理。enumPossibleType{Foo="Foo",Bar="Bar",Baz="Baz",}functionchecker(input:PossibleType){switch(input){casePossibleType.Foo:console.log("foo!");break;casePossibleType.Bar:console.log("bar!");break;casePossibleType.Baz:console.log("baz!");break;default:const_exhaustiveCheck:never=input;break;}}以上就是我们目前的情况using一些规则,以及一批规则涉及到定制化程度高或者适用场景狭窄,这里就不一一列举了。如果你有什么想法,欢迎和我交流,但请注意:我不是在灌输你必须使用什么规则,我只是分享我们使用的规则和注意事项,所以请确认你不属于这种点留言前的看法,感谢阅读。参考文献[1]QCon+Topic:TypeScript在中大型项目中的实现:https://qconplus.infoq.cn/2021/beijing2nth/track/1240[2]TypeScript研发协议在淘宝店铺的实现:https://qconplus.infoq.cn/2021/beijing2nth/presentation/4161[3]提案错误原因:https://github.com/tc39/proposal-error-cause[4]strictFunctionTypes:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types