Typescript 嚴格模式有多嚴格?

前言

「媽媽,我想寫TypeScript」,「報錯這麼多了,夠嗎孩子?」html

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

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

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

1. Typescript嚴格模式規則

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

規則名稱 解釋
noImplicitAny 不容許變量或函數參數具備隱式any類型。
noImplicitThis 不容許this上下文隱式定義。
strictNullChecks 不容許出現nullundefined的可能性。
strictPropertyInitialization 驗證構造函數內部初始化先後已定義的屬性。
strictBindCallApply bind, call, apply 更嚴格的類型檢測。
strictFunctionTypes 對函數參數進行嚴格逆變比較。

2. 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)
}
複製代碼

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

瀏覽器自帶事件,好比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)
}
複製代碼

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

此規則不容許出現nullundefined的可能性。請看如下示例:

// 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
}
複製代碼

「真香!」

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<number>((total, num) => total + num, 0)
}

sum.apply(null, [1, 2, 3])
// 6
複製代碼

7. strictFunctionTypes

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

初看,心裏OS: 「這什麼玩意兒?」,這裏有篇介紹:

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

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

  • 子類型能夠隱性的轉換爲父類型

說個最容易理解的例子,intfloat兩個類型的關係能夠寫成下面這樣。 intfloat :也就是說intfloat的子類型。

這一更嚴格的檢查應用於除方法或構造函數聲明之外的全部函數類型。方法被專門排除在外是爲了確保帶泛型的類和接口(如 Array )整體上仍然保持協變。

請看下面這個 AnimalDogCat 的父類型的例子:

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<T> {
    compare: (a: T, b: T) => number;
}

declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;

animalComparer = dogComparer;  // 錯誤
dogComparer = animalComparer;  // 正確
複製代碼

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

總結&參考

參考文章:

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

在面試的過程當中,常被問到爲何TypescriptJavaScript好用?

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

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

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

也能夠來個人GitHub博客裏拿全部文章的源文件:

前端勸退指南github.com/roger-hiro/…

相關文章
相關標籤/搜索