\n\n
当Typescript
严格模式设置为on
时,它将使用strict
族下的严格类型规则对项目中的所有文件进行代码验证。规则是:
规则名称 | \n\t\t\t解释 | \n\t\t
---|---|
noImplicitAny | \n\t\t\t不允许变量或函数参数具有隐式any 类型。 | \n\t\t
noImplicitThis | \n\t\t\t不允许this 上下文隐式定义。 | \n\t\t
strictNullChecks | \n\t\t\t不允许出现null 或undefined 的可能性。 | \n\t\t
strictPropertyInitialization | \n\t\t\t验证构造函数内部初始化前后已定义的属性。 | \n\t\t
strictBindCallApply | \n\t\t\t对bind, call, apply 更严格的类型检测。 | \n\t\t
strictFunctionTypes | \n\t\t\t对函数参数进行严格逆变比较。 | \n\t\t
noImplicitAny
此规则不允许变量或函数参数具有隐式any
类型。请看以下示例:
\n// Javascript/Typescript 非严格模式\nfunction extractIds (list) {\n return list.map(member => member.id)\n}\n
\n\n上述例子没有对list
进行类型限制,map
循环了item
的形参member
。而在Typescript
严格模式下,会出现以下报错:
\n// Typescript 严格模式\nfunction extractIds (list) {\n // ❌ ^^^^\n // Parameter 'list' implicitly\n // has an 'any' type. ts(7006)\n return list.map(member => member.id)\n // ❌ ^^^^^^\n // Parameter 'member' implicitly\n // has an 'any' type. ts(7006)\n}\n
\n\n正确写法应是:
\n\n\n// Typescript 严格模式\ninterface Member {\n id: number\n name: string\n}\n\nfunction extractIds (list: Member[]) {\n return list.map(member => member.id)\n}\n
\n\n浏览器自带事件,比如e.preventDefault()
,是阻止浏览器默认行为的关键代码。
这在Typescript
严格模式下是会报错的:
\n// Typescript 严格模式\nfunction onChangeCheckbox (e) {\n // ❌ ^\n // Parameter 'e' implicitly\n // has an 'any' type. ts(7006)\n e.preventDefault()\n const value = e.target.checked\n validateCheckbox(value)\n}\n
\n\n\n\n若需要正常使用这类Web API
,就需要在全局定义扩展。比如:
\n// Typescript 严格模式\ninterface ChangeCheckboxEvent extends MouseEvent {\n target: HTMLInputElement\n}\n\nfunction onChangeCheckbox (e: ChangeCheckboxEvent) {\n e.preventDefault()\n const value = e.target.checked\n validateCheckbox(value)\n}\n
\n\n请注意,如果导入了非Typescript
库,这也会引发错误,因为导入的库的类型是any
。
\n// Typescript 严格模式\nimport { Vector } from 'sylvester'\n// ❌ ^^^^^^^^^^^\n// Could not find a declaration file\n// for module 'sylvester'.\n// 'sylvester' implicitly has an 'any' type.\n// Try `npm install @types/sylvester`\n// if it exists or add a new declaration (.d.ts)\n// file containing `declare module 'sylvester';`\n// ts(7016)\n
\n\n这可能是项目重构Typescript
版的一大麻烦,需要专门定义第三方库接口类型
noImplicitThis
此规则不允许this
上下文隐式定义。请看以下示例:
\n// Javascript/Typescript 非严格模式\nfunction uppercaseLabel () {\n return this.label.toUpperCase()\n}\n\nconst config = {\n label: 'foo-config',\n uppercaseLabel\n}\n\nconfig.uppercaseLabel()\n// FOO-CONFIG\n
\n\n在非严格模式下,this
指向config
对象。this.label
只需检索config.label
。
但是,this
在函数上进行引用可能是不明确的:
\n// Typescript严格模式\nfunction uppercaseLabel () {\n return this.label.toUpperCase()\n // ❌ ^^^^\n // 'this' implicitly has type 'any'\n // because it does not have a type annotation. ts(2683)\n}\n
\n\n如果单独执行this.label.toUpperCase()
,则会因为this
上下文config
不再存在而报错,因为label
未定义。
解决该问题的一种方法是避免this
在没有上下文的情况下使用函数:
\n// Typescript严格模式\nconst config = {\n label: 'foo-config',\n uppercaseLabel () {\n return this.label.toUpperCase()\n }\n}\n
\n\n更好的方法是编写接口,定义所有类型,而不是Typescript
来推断:
\n// Typescript严格模式\ninterface MyConfig {\n label: string\n uppercaseLabel: (params: void) => string\n}\n\nconst config: MyConfig = {\n label: 'foo-config',\n uppercaseLabel () {\n return this.label.toUpperCase()\n }\n}\n
\n\nstrictNullChecks
此规则不允许出现null
或undefined
的可能性。请看以下示例:
\n// Typescript 非严格模式\nfunction getArticleById (articles: Article[], id: string) {\n const article = articles.find(article => article.id === id)\n return article.meta\n}\n
\n\nTypescript
非严格模式下,这样写不会有任何问题。但严格模式会非给你搞出点幺蛾子:
“你这样不行,万一find
没有匹配到任何值呢?”:
\n// Typescript严格模式\nfunction getArticleById (articles: Article[], id: string) {\n const article = articles.find(article => article.id === id)\n return article.meta\n // ❌ ^^^^^^^\n // Object is possibly 'undefined'. ts(2532)\n}\n
\n\n“我星星你个星星!”
\n\n于是你会将改成以下模样:
\n\n\n// Typescript严格模式\nfunction getArticleById (articles: Article[], id: string) {\n const article = articles.find(article => article.id === id)\n if (typeof article === 'undefined') {\n throw new Error(`Could not find an article with id: ${id}.`)\n }\n\n return article.meta\n}\n
\n\n“真香!”
\n\n\n\nstrictPropertyInitialization
此规则将验证构造函数内部初始化前后已定义的属性。
\n\n必须要确保每个实例的属性都有初始值,可以在构造函数里或者属性定义时赋值。
\n\n(strictPropertyInitialization
,这臭长的命名像极了React
源码里的众多任性属性)
请看以下示例:
\n\n\n// Typescript非严格模式\nclass User {\n username: string;\n}\n\nconst user = new User();\n\nconst username = user.username.toLowerCase();\n
\n\n如果启用严格模式,类型检查器将进一步报错:
\n\n\nclass User {\n username: string;\n // ❌ ^^^^^^\n // Property 'username' has no initializer\n // and is not definitely assigned in the constructor\n}\n\nconst user = new User();\n/\nconst username = user.username.toLowerCase();\n // ❌ ^^^^^^^^^^^^\n// TypeError: Cannot read property 'toLowerCase' of undefined\n
\n\n解决方案有四种。
\n\nundefined
为username
属性定义提供一个undefined
类型:
\nclass User {\n username: string | undefined;\n}\n\nconst user = new User();\n
\n\nusername
属性可以为string | undefined
类型,但这样写,需要在使用时确保值为string
类型:
\nconst username = typeof user.username === "string"\n ? user.username.toLowerCase()\n : "n/a";\n
\n\n这也太不Typescript
了。
这个方法有点笨,却挺有效:
\n\n\nclass User {\n username = "n/a";\n}\n\nconst user = new User();\n\n// OK\nconst username = user.username.toLowerCase();\n
\n\n最有用的解决方案是向username
构造函数添加参数,然后将其分配给username
属性。
这样,无论何时new User()
,都必须提供默认值作为参数:
\nclass User {\n username: string;\n\n constructor(username: string) {\n this.username = username;\n }\n}\n\nconst user = new User("mariusschulz");\n\n// OK\nconst username = user.username.toLowerCase();\n
\n\n还可以通过public
修饰符进一步简化:
\nclass User {\n constructor(public username: string) {}\n}\n\nconst user = new User("mariusschulz");\n\n// OK\nconst username = user.username.toLowerCase();\n
\n\n在某些场景下,属性会被间接地初始化(使用辅助方法或依赖注入库)。
\n\n这种情况下,你可以在属性上使用显式赋值断言来帮助类型系统识别类型。
\n\n\nclass User {\n username!: string;\n\n constructor(username: string) {\n this.initialize(username);\n }\n\n private initialize(username: string) {\n this.username = username;\n }\n}\n\nconst user = new User("mariusschulz");\n\n// OK\nconst username = user.username.toLowerCase();\n
\n\n通过向该username
属性添加一个明确的赋值断言,我们告诉类型检查器:username
,即使它自己无法检测到该属性,也可以期望该属性被初始化。
strictBindCallApply
此规则将对bind, call, apply
更严格地检测类型。
啥意思?请看以下示例:
\n\n\n// JavaScript\nfunction sum (num1: number, num2: number) {\n return num1 + num2\n}\n\nsum.apply(null, [1, 2])\n// 3\n
\n\n在你不记得参数类型时,非严格模式下不会校验参数类型和数量,运行代码时,Typescript
和环境(可能是浏览器)都不会引发错误:
\n// Typescript非严格模式\nfunction sum (num1: number, num2: number) {\n return num1 + num2\n}\n\nsum.apply(null, [1, 2, 3])\n// 还是...3?\n
\n\n而Typescript
严格模式下,这是不被允许的:
\n// Typescript严格模式\nfunction sum (num1: number, num2: number) {\n return num1 + num2\n}\n\nsum.apply(null, [1, 2, 3])\n// ❌ ^^^^^^^^^\n// Argument of type '[number, number, number]' is not\n// assignable to parameter of type '[number, number]'.\n// Types of property 'length' are incompatible.\n// Type '3' is not assignable to type '2'. ts(2345)\n
\n\n那怎么办?“...”
扩展运算符和reduce
老友来相救:
\n// Typescript严格模式\nfunction sum (...args: number[]) {\n return args.reduce<number>((total, num) => total + num, 0)\n}\n\nsum.apply(null, [1, 2, 3])\n// 6\n
\n\n该规则将检查并限制函数类型参数是抗变(contravariantly
)而非双变(bivariantly
,即协变或抗变)的。
初看,内心 OS:“这什么玩意儿?”,这里有篇介绍:
\n\n\n\n\n协变(covariance)和抗变(contravariance)是什么?[1]
\n
协变和逆变维基上写的很复杂,但是总结起来原理其实就一个。
\n\n说个最容易理解的例子,int
和float
两个类型的关系可以写成下面这样。int
≦float
:也就是说int
是float
的子类型。
这一更严格的检查应用于除方法或构造函数声明以外的所有函数类型。方法被专门排除在外是为了确保带泛型的类和接口(如 Array )总体上仍然保持协变。
\n\n请看下面这个Animal
是Dog
和Cat
的父类型的例子:
\ndeclare let f1: (x: Animal) => void;\ndeclare let f2: (x: Dog) => void;\ndeclare let f3: (x: Cat) => void;\nf1 = f2; // 启用 --strictFunctionTypes 时错误\nf2 = f1; // 正确\nf2 = f3; // 错误\n
\n\n用另一种方式来描述这个例子则是,默认类型检查模式中T
在类型(x: T) => void
是 双变的,但在严格函数类型模式中T
是 抗变的:
\ninterface Comparer<T> {\n compare: (a: T, b: T) => number;\n}\n\ndeclare let animalComparer: Comparer<Animal>;\ndeclare let dogComparer: Comparer<Dog>;\n\nanimalComparer = dogComparer; // 错误\ndogComparer = animalComparer; // 正确\n
\n\n\n\n写到此处,逼死了一个菜鸡前端。
\n\n参考文章:
\n\n\n\n\n\n\t
\n- \n\t
\n\tHow strict is Typescript’s strict mode?[2] \n\t- \n\t
\n\t应该怎么理解编程语言中的协变逆变?[3] \n\t- \n\t
\nTypeScript 严格函数类型[4] \n\t
在面试的过程中,常被问到为什么Typescript
比JavaScript
好用?
从这些严格模式规则,你就可以一窥当中的奥秘,今日开严格,他日 Bug 秒甩锅,噢耶。
\n\n\n\n\n\n
\n\n
协变(covariance)和抗变(contravariance)是什么?:https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance
\n[2]\n\nHow strict is Typescript’s strict mode?:https://medium.com/swlh/how-strict-is-typescripts-strict-mode-f36a4d1a948a
\n[3]\n\n应该怎么理解编程语言中的协变逆变?:https://www.zhihu.com/question/38861374
\n[4]\n\nTypeScript 严格函数类型:https://www.tslang.cn/docs/release-notes/typescript-2.6.html
\n\n\n
\n","id":71939,"author_id":181,"state":1,"type":0,"create_time":1591884833,"update_time":1597917058}}}}}