「媽媽,我想寫TypeScript」,「報錯這麼多了,夠嗎孩子?」html
"use strict"
指令在 JavaScript 1.8.5 (ECMAScript5)
中新增。前端
至今,前端er們基本都默認開啓嚴格模式敲代碼。git
那麼,你知道Typescript
其實也有屬於本身的嚴格模式嗎?github
Typescript
嚴格模式規則當Typescript
嚴格模式設置爲on
時,它將使用strict
族下的嚴格類型規則對項目中的全部文件進行代碼驗證。規則是:面試
規則名稱 | 解釋 |
---|---|
noImplicitAny |
不容許變量或函數參數具備隱式any 類型。 |
noImplicitThis |
不容許this 上下文隱式定義。 |
strictNullChecks |
不容許出現null 或undefined 的可能性。 |
strictPropertyInitialization |
驗證構造函數內部初始化先後已定義的屬性。 |
strictBindCallApply |
對 bind, call, apply 更嚴格的類型檢測。 |
strictFunctionTypes |
對函數參數進行嚴格逆變比較。 |
noImplicitAny
此規則不容許變量或函數參數具備隱式any
類型。請看如下示例:typescript
// Javascript/Typescript 非嚴格模式
function extractIds (list) {
return list.map(member => member.id)
}
複製代碼
上述例子沒有對list
進行類型限制,map
循環了item
的形參member
。 而在Typescript
嚴格模式下,會出現如下報錯:npm
// Typescript 嚴格模式
function extractIds (list) {
// ❌ ^^^^
// Parameter 'list' implicitly
// has an 'any' type. ts(7006)
return list.map(member => member.id)
// ❌ ^^^^^^
// Parameter 'member' implicitly
// has an 'any' type. ts(7006)
}
複製代碼
正確寫法應是:編程
// Typescript 嚴格模式
interface Member {
id: number
name: string
}
function extractIds (list: Member[]) {
return list.map(member => member.id)
}
複製代碼
瀏覽器自帶事件,好比e.preventDefault()
,是阻止瀏覽器默認行爲的關鍵代碼。瀏覽器
這在Typescript
嚴格模式下是會報錯的:bash
// Typescript 嚴格模式
function onChangeCheckbox (e) {
// ❌ ^
// Parameter 'e' implicitly
// has an 'any' type. ts(7006)
e.preventDefault()
const value = e.target.checked
validateCheckbox(value)
}
複製代碼
若須要正常使用這類
Web API
,就須要在全局定義擴展。好比:
// Typescript 嚴格模式
interface ChangeCheckboxEvent extends MouseEvent {
target: HTMLInputElement
}
function onChangeCheckbox (e: ChangeCheckboxEvent) {
e.preventDefault()
const value = e.target.checked
validateCheckbox(value)
}
複製代碼
請注意,若是導入了非Typescript
庫,這也會引起錯誤,由於導入的庫的類型是any
。
// Typescript 嚴格模式
import { Vector } from 'sylvester'
// ❌ ^^^^^^^^^^^
// Could not find a declaration file
// for module 'sylvester'.
// 'sylvester' implicitly has an 'any' type.
// Try `npm install @types/sylvester`
// if it exists or add a new declaration (.d.ts)
// file containing `declare module 'sylvester';`
// ts(7016)
複製代碼
這多是項目重構Typescript
版的一大麻煩,須要專門定義第三方庫接口類型
noImplicitThis
此規則不容許this
上下文隱式定義。請看如下示例:
// Javascript/Typescript 非嚴格模式
function uppercaseLabel () {
return this.label.toUpperCase()
}
const config = {
label: 'foo-config',
uppercaseLabel
}
config.uppercaseLabel()
// FOO-CONFIG
複製代碼
在非嚴格模式下,this
指向config
對象。this.label
只需檢索config.label
。
可是,this
在函數上進行引用多是不明確的:
// Typescript嚴格模式
function uppercaseLabel () {
return this.label.toUpperCase()
// ❌ ^^^^
// 'this' implicitly has type 'any'
// because it does not have a type annotation. ts(2683)
}
複製代碼
若是單獨執行this.label.toUpperCase()
,則會由於this
上下文config
再也不存在而報錯,由於label
未定義。
解決該問題的一種方法是避免this
在沒有上下文的狀況下使用函數:
// Typescript嚴格模式
const config = {
label: 'foo-config',
uppercaseLabel () {
return this.label.toUpperCase()
}
}
複製代碼
更好的方法是編寫接口,定義全部類型,而不是Typescript
來推斷:
// Typescript嚴格模式
interface MyConfig {
label: string
uppercaseLabel: (params: void) => string
}
const config: MyConfig = {
label: 'foo-config',
uppercaseLabel () {
return this.label.toUpperCase()
}
}
複製代碼
strictNullChecks
此規則不容許出現null
或undefined
的可能性。請看如下示例:
// Typescript 非嚴格模式
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
return article.meta
}
複製代碼
Typescript
非嚴格模式下,這樣寫不會有任何問題。但嚴格模式會非給你搞出點幺蛾子:
「你這樣不行,萬一find
沒有匹配到任何值呢?」:
// Typescript嚴格模式
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
return article.meta
// ❌ ^^^^^^^
// Object is possibly 'undefined'. ts(2532)
}
複製代碼
「我星星你個星星!」
因而你會將改爲如下模樣:
// Typescript嚴格模式
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
if (typeof article === 'undefined') {
throw new Error(`Could not find an article with id: ${id}.`)
}
return article.meta
}
複製代碼
「真香!」
strictPropertyInitialization
此規則將驗證構造函數內部初始化先後已定義的屬性。
必需要確保每一個實例的屬性都有初始值,能夠在構造函數裏或者屬性定義時賦值。
(strictPropertyInitialization
,這臭長的命名像極了React
源碼裏的衆多任性屬性)
請看如下示例:
// Typescript非嚴格模式
class User {
username: string;
}
const user = new User();
const username = user.username.toLowerCase();
複製代碼
若是啓用嚴格模式,類型檢查器將進一步報錯:
class User {
username: string;
// ❌ ^^^^^^
// Property 'username' has no initializer
// and is not definitely assigned in the constructor
}
const user = new User();
/
const username = user.username.toLowerCase();
// ❌ ^^^^^^^^^^^^
// TypeError: Cannot read property 'toLowerCase' of undefined
複製代碼
解決方案有四種。
undefined
爲username
屬性定義提供一個undefined
類型:
class User {
username: string | undefined;
}
const user = new User();
複製代碼
username
屬性能夠爲string | undefined
類型,但這樣寫,須要在使用時確保值爲string
類型:
const username = typeof user.username === "string"
? user.username.toLowerCase()
: "n/a";
複製代碼
這也太不Typescript
了。
這個方法有點笨,卻挺有效:
class User {
username = "n/a";
}
const user = new User();
// OK
const username = user.username.toLowerCase();
複製代碼
最有用的解決方案是向username
構造函數添加參數,而後將其分配給username
屬性。
這樣,不管什麼時候new User()
,都必須提供默認值做爲參數:
class User {
username: string;
constructor(username: string) {
this.username = username;
}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
複製代碼
還能夠經過public
修飾符進一步簡化:
class User {
constructor(public username: string) {}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
複製代碼
在某些場景下,屬性會被間接地初始化(使用輔助方法或依賴注入庫)。
這種狀況下,你能夠在屬性上使用顯式賦值斷言來幫助類型系統識別類型。
class User {
username!: string;
constructor(username: string) {
this.initialize(username);
}
private initialize(username: string) {
this.username = username;
}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
複製代碼
經過向該username
屬性添加一個明確的賦值斷言,咱們告訴類型檢查器: username
,即便它本身沒法檢測到該屬性,也能夠指望該屬性被初始化。
strictBindCallApply
此規則將對 bind, call, apply
更嚴格地檢測類型。
啥意思?請看如下示例:
// JavaScript
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2])
// 3
複製代碼
在你不記得參數類型時,非嚴格模式下不會校驗參數類型和數量,運行代碼時,Typescript
和環境(多是瀏覽器)都不會引起錯誤:
// Typescript非嚴格模式
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2, 3])
// 仍是...3?
複製代碼
而Typescript
嚴格模式下,這是不被容許的:
// Typescript嚴格模式
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2, 3])
// ❌ ^^^^^^^^^
// Argument of type '[number, number, number]' is not
// assignable to parameter of type '[number, number]'.
// Types of property 'length' are incompatible.
// Type '3' is not assignable to type '2'. ts(2345)
複製代碼
那怎麼辦? 「...」
擴展運算符和reduce
老友來相救:
// Typescript嚴格模式
function sum (...args: number[]) {
return args.reduce<number>((total, num) => total + num, 0)
}
sum.apply(null, [1, 2, 3])
// 6
複製代碼
該規則將檢查並限制函數類型參數是抗變(contravariantly
)而非雙變(bivariantly
,即協變或抗變)的。
初看,心裏OS: 「這什麼玩意兒?」,這裏有篇介紹:
協變和逆變維基上寫的很複雜,可是總結起來原理其實就一個。
說個最容易理解的例子,int
和float
兩個類型的關係能夠寫成下面這樣。 int
≦ float
:也就是說int
是float
的子類型。
這一更嚴格的檢查應用於除方法或構造函數聲明之外的全部函數類型。方法被專門排除在外是爲了確保帶泛型的類和接口(如 Array )整體上仍然保持協變。
請看下面這個 Animal
是 Dog
和 Cat
的父類型的例子:
declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f1 = f2; // 啓用 --strictFunctionTypes 時錯誤
f2 = f1; // 正確
f2 = f3; // 錯誤
複製代碼
用另外一種方式來描述這個例子則是,默認類型檢查模式中 T
在類型 (x: T) => void
是 雙變的,但在嚴格函數類型模式中 T
是 抗變的:
interface Comparer<T> {
compare: (a: T, b: T) => number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // 錯誤
dogComparer = animalComparer; // 正確
複製代碼
寫到此處,逼死了一個菜雞前端。
參考文章:
在面試的過程當中,常被問到爲何Typescript
比JavaScript
好用?
從這些嚴格模式規則,你就能夠一窺當中的奧祕,今日開嚴格,他日Bug秒甩鍋,噢耶。
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
也能夠來個人GitHub
博客裏拿全部文章的源文件:
前端勸退指南:github.com/roger-hiro/…