Typescript 嚴格模式有多嚴格?

前言

"use strict"指令在JavaScript 1.8.5 (ECMAScript5)中新增。

至今,前端 er 們基本都默認開啟嚴格模式敲代碼。

那麼,你知道Typescript其實也有屬於自己的嚴格模式嗎?

1.Typescript嚴格模式規則

Typescript 嚴格模式有多嚴格?

當Typescript嚴格模式設置為on時,它將使用strict族下的嚴格類型規則對項目中的所有文件進行代碼驗證。規則是:

  • noImplicitAny,不允許變量或函數參數具有隱式any類型。
  • noImplicitThis,不允許this上下文隱式定義。
  • strictNullChecks,不允許出現null或undefined的可能性。
  • strictPropertyInitialization,驗證構造函數內部初始化前後已定義的屬性。
  • strictBindCallApply,對bind, call, apply更嚴格的類型檢測。
  • strictFunctionTypes,對函數參數進行嚴格逆變比較。

2.noImplicitAny

此規則不允許變量或函數參數具有隱式any類型。請看以下示例:

// Javascript/Typescript 非嚴格模式
function extractIds (list) {
 return list.map(member => member.id)
}

上述例子沒有對list進行類型限制,map循環了item的形參member。而在Typescript嚴格模式下,會出現以下報錯:

// 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)
}

1.1 瀏覽器自帶事件該如何處理?

瀏覽器自帶事件,比如e.preventDefault(),是阻止瀏覽器默認行為的關鍵代碼。

這在Typescript嚴格模式下是會報錯的:

// Typescript 嚴格模式
function onChangeCheckbox (e) {
 // ❌ ^
 // Parameter 'e' implicitly
 // has an 'any' type. ts(7006)
 e.preventDefault()
 const value = e.target.checked
 validateCheckbox(value)
}
Typescript 嚴格模式有多嚴格?

若需要正常使用這類Web API,就需要在全局定義擴展。比如:

// Typescript 嚴格模式
interface ChangeCheckboxEvent extends MouseEvent {
 target: HTMLInputElement
}
function onChangeCheckbox (e: ChangeCheckboxEvent) {
 e.preventDefault()
 const value = e.target.checked
 validateCheckbox(value)
}

1.2 第三方庫也需定義好類型

請注意,如果導入了非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版的一大麻煩,需要專門定義第三方庫接口類型

3.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()
 }
}

4.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
}

“真香!”

Typescript 嚴格模式有多嚴格?

5.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

解決方案有四種。

方案#1:允許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了。

方案#2:屬性值顯式初始化

這個方法有點笨,卻挺有效:

class User {
 username = "n/a";
}
const user = new User();
// OK
const username = user.username.toLowerCase();

方案#3:在構造函數中賦值

最有用的解決方案是向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();

方案#4:顯式賦值斷言

在某些場景下,屬性會被間接地初始化(使用輔助方法或依賴注入庫)。

這種情況下,你可以在屬性上使用顯式賦值斷言來幫助類型系統識別類型。

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,即使它自己無法檢測到該屬性,也可以期望該屬性被初始化。

6.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((total, num) => total + num, 0)
}
sum.apply(null, [1, 2, 3])
// 6

7. strictFunctionTypes

該規則將檢查並限制函數類型參數是抗變(contravariantly)而非雙變(bivariantly,即協變或抗變)的。

初看,內心 OS:“這什麼玩意兒?”,這裡有篇介紹:

協變(covariance)和抗變(contravariance)是什麼?[1]

協變和逆變維基上寫的很複雜,但是總結起來原理其實就一個。

  • 子類型可以隱性的轉換為父類型

說個最容易理解的例子,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; // 錯誤
  1. 第一個賦值語句在默認的類型檢查模式中是允許的,但是在嚴格函數類型模式下會被標記錯誤。
  2. 而嚴格函數類型模式將它標記為錯誤,因為它不能 被證明合理。
  3. 任何一種模式中,第三個賦值都是錯誤的,因為它 永遠不合理。

用另一種方式來描述這個例子則是,默認類型檢查模式中T在類型(x: T) => void是 雙變的,但在嚴格函數類型模式中T是 抗變的:

interface Comparer {
 compare: (a: T, b: T) => number;
}
declare let animalComparer: Comparer;
declare let dogComparer: Comparer;
animalComparer = dogComparer; // 錯誤
dogComparer = animalComparer; // 正確
Typescript 嚴格模式有多嚴格?

寫到此處,逼死了一個菜雞前端。

總結&參考

參考文章:

  • How strict is Typescript’s strict mode?[2]
  • 應該怎麼理解編程語言中的協變逆變?[3]
  • TypeScript 嚴格函數類型[4]

在面試的過程中,常被問到為什麼Typescript比JavaScript好用?

從這些嚴格模式規則,你就可以一窺當中的奧秘,今日開嚴格,他日 Bug 秒甩鍋,噢耶。

❤️ 看完三件事

如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注「前端勸退師」,不定期分享原創知識。
  3. 也看看其它文章

也可以來我的GitHub博客裡拿所有文章的源文件:

前端勸退指南

:https://github.com/roger-hiro/BlogFN

參考資料

[1]協變(covariance)和抗變(contravariance)是什麼?:https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance

[2]How strict is Typescript’s strict mode?:https://medium.com/swlh/how-strict-is-typescripts-strict-mode-f36a4d1a948a

[3]應該怎麼理解編程語言中的協變逆變?:https://www.zhihu.com/question/38861374

[4]TypeScript 嚴格函數類型:https://www.tslang.cn/docs/release-notes/typescript-2.6.html


分享到:


相關文章: