您好,登錄后才能下訂單哦!
這篇文章主要講解了“TypeScript 4.2有哪些新優點”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“TypeScript 4.2有哪些新優點”吧!
開始使用 TypeScript 4.2,你可以通過 NuGet[2] 安裝,或使用 npm 運行以下命令:
npm install typescript
讓我們預覽一下 TypeScript 4.2 的新特性:
更智能的類型別名保持
元組類型中的前置/中置的擴展元素
更嚴格的 in 運算符檢查
--noPropertyAccessFromIndexSignature
抽象構建簽名
通過 --explainFiles 了解你的項目結構
改進邏輯表達式中非執行函數的檢查
可以將解構變量明確標記為未使用
在可選屬性和字符串索引簽名之間寬松的規則
聲明缺少的幫助函數
破壞性變化
TypeScript 有一種方式可以聲明新的類型叫做類型別名。如果你寫了一系列函數可以處理 string | number | boolean 三種類型的數據,那么你可以定義一個類型別名,用于避免重復工作:
type BasicPrimitive = number | string | boolean
TypeScript 曾使用一系列的規則來猜測何時應當使用類型別名,而何時又應該把所有類型都打印出來。例如,來看下以下代碼片段:
export type BasicPrimitive = number | string | boolean; export function doStuff(value: BasicPrimitive) { let x = value; return x; }
如果我們在諸如 Visual Studio、VS Code 編輯器中或者 TypeScript 運行環境[3] 中把鼠標懸停在變量 x 上,我們就會看到一個快速展示面板,顯示出 x 的類型為 BasicPrimitive。同樣的,如果我們為這個方法定義一個聲明文件(.d.ts 文件),TypeScript 將會顯示 doStuff 返回值的類型為 BasicPrimitive。
但是,如果我們返回的是 BasicPrimitive 或者 undefined 會發生什么呢?
export type BasicPrimitive = number | string | boolean; export function doStuff(value: BasicPrimitive) { if (Math.random() < 0.5) { return undefined; } return value; }
我們可以在TypeScript 運行環境[4]中觀察發生了什么。盡管我們希望看到 TypeScript 展示的 doStuff 返回值類型是 BasicPrimitive | undefined,但是實際情況是,顯示的返回值類型為 string | number | boolean | undefined!這是怎么回事?
這要歸因于 TypeScript 內部對于類型的解析方式。當創建了一個聯合類型包含一個或多個其他的聯合類型時,TypeScript 會將這些類型歸一化為一個新的扁平的聯合類型 —— 此時,原本類型的信息就丟失了。類型檢查器將會查找 string | number | boolean | undefined 每種組合是否具有類型別名,即使這樣,仍可能會得到多個 string | number | boolean 類型別名的結果。
在 TypeScript 4.2 中,我們的內部邏輯將更加智能。我們會通過保留類型的原始定義以及后續對其的更新,從而持續追蹤該類型的構造變化。我們同時也會追蹤被鍵入其他類型別名實例的類型別名,并加以區分。
能夠根據你在代碼中使用的方式打印出這些類型,意味著對于 TypeScript 使用者來說,可以避免看到那些令人厭惡的巨型類型定義;并且這種方式可以幫助轉化出更優質的 .d.ts 文件輸出、錯誤信息以及在編輯器中對變量展示的快速信息和幫助等。
更詳細的內容,可以查閱改進保留實例間聯合和相交類型別名的第一個Pull Request[5],以及隨后的保留間接別名的第二個Pull Request[6]。
在 TypeScript 中,元組類型最初用于對特定長度和特定元素類型進行建模。
// 一個存放了一對數字的元組 let a: [number, number] = [1, 2]; // 一個存放了一個字符串、一個數字以及一個布爾值的元組 let b: [string, number, boolean] = ["hello", 42, true];
隨著版本的更新,TypeScript 元組變得越來越復雜,因為它們還被用于對像 JavaScript 中的參數列表一樣的事務進行建模。這樣帶來的結果就是,元組類型可以包含可選元素以及擴展元素,甚至為了提高可讀性和易用性,元素還可以擁有標簽。
// 一個包含一個或兩個字符串的元組 let c: [string, string?] = ["hello"]; c = ["hello", "world"]; // 一個包含一個或兩個字符串,并且打了標簽的元組 let d: [first: string, second?: string] = ["hello"]; d = ["hello", "world"]; // 一個包含擴展元素的元組 —— 前置元素至少包含兩個字符串 // 后置元素可以包含任意多個布爾值 let e: [string, string, ...boolean[]]; e = ["hello", "world"]; e = ["hello", "world", false]; e = ["hello", "world", true, false, true];
在 TypeScript 4.2 中,擴展元素的使用特別得到了擴展。在較早的版本中,TypeScript 只允許擴展元素出現在元組的最后位置。
但是現在,擴展元素可以出現在元組的任意位置 —— 僅僅需要滿足一些限制條件。
let foo: [...string[], number]; foo = [123]; foo = ["hello", 123]; foo = ["hello!", "hello!", "hello!", 123]; let bar: [boolean, ...string[], boolean]; bar = [true, false]; bar = [true, "some text", false]; bar = [true, "some", "separated", "text", false];
想讓擴展元素放在元組的任意位置,唯一的限制條件就是:不能有可選元素在其后面的位置并且不能有其他的擴展元素。換句話說,每個元組只允許一個擴展元素,并且該擴展元素后序位置不能跟隨一個可選元素。
interface Clown { /*...*/ } interface Joker { /*...*/ } let StealersWheel: [...Clown[], "me", ...Joker[]]; // ~~~~~~~~~~ Error! // 擴展元素后續不能有其他的擴展元素 let StringsAndMaybeBoolean: [...string[], boolean?]; // ~~~~~~~~ Error! // 擴展元素后序不能是可選元素
這些靈活的擴展元素,可以用于對具有任意數量前置參數以及固定格式后置參數的函數進行建模。
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void; doStuff(/*shouldCapitalize:*/ false) doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
盡管 JavaScript 沒有任何語法可以支持前置的擴展參數,我們仍可以使用 ...args 這種元組類型來實現上述 doStuff 那樣具有前置擴展參數的函數。用這種方式可以對許多現有的 JavaScript 進行建模。
更多詳細信息,請參考這個 Pull Request[7]。
在 JavaScript 中,在 in 運算符右側使用非對象類型是一個運行時錯誤。而在 TypeScript 4.2 中,該錯誤可以在代碼設計時即被捕獲。
"foo" in 42 // ~~ // 錯誤!'in' 表達式右側不能為基數據類型
這項檢查在大多數情況下都是相當保守的,如果你遇到過相關的錯誤提示,很可能是代碼出了問題。
非常感謝我們的外圍貢獻者 Jonas Hübotter[8] 提出的 Pull Request[9]。
在 TypeScript 首次引入索引簽名時,你只能使用諸如 person["name"] 這種 “中括號” 元素訪問語法來聲明對象的屬性。
interface SomeType { /** 這是一個索引簽名 */ [propName: string]: any; } function doStuff(value: SomeType) { let x = value["someProperty"]; }
如果我們需要一個具有任意屬性的對象,這種方式將會非常麻煩。舉個例子,假設有一個 API,犯了一個常見的拼寫錯誤——在某個屬性名字后面多加了一個 s。
interface Options { /** 需要排除的文件格式 */ exclude?: string[]; /** * 處理未聲明為 'any' 的所有其他屬性 */ [x: string]: any; } function processOptions(opts: Options) { // 注意這里我們故意訪問的是 `excludes` 而非 `exclude` if (opts.excludes) { console.error("The option `excludes` is not valid. Did you mean `exclude`?"); } }
為了使這些類型更易用,前不久,TypeScript 允許了使用 “.” 方法訪問具有字符串索引簽名對象(例如:person.name)的屬性的語法。這也使得從現有 JavaScript 代碼過渡為 TypeScript 代碼變得容易。
但是,放松限制同時也意味著拼寫錯誤導致的顯示聲明屬性的錯誤訪問會更加容易。
function processOptions(opts: Options) { // ... // 注意,這次我們是 “無意間” 訪問了 `excludes` // 糟糕!完全奏效。 for (const excludePattern of opts.excludes) { // ... } }
某些情況下,用戶只希望在顯示聲明的索引簽名中進行訪問——他們希望在使用點方法訪問對象屬性時,如果該屬性不具有顯示聲明,則應當返回錯誤信息。
這就是 TypeScript 引入新的標識 --noPropertyAccessFromIndexSignature 的目的。開啟這么模式,你將選擇使用 TypeScript 舊的驗證行為,從而在上述過程中拋出錯誤。這個新的設置不受嚴格模式的限制,因為我們相信用戶會發現它在某些特定代碼中會很有用。
你可以通過閱讀這個 Pull Request[10] 獲取對這個功能更詳細的了解。同時,我們還要非常感謝 Wenlu Wang[11] 向我們提交了這個 Pull Request。
TypeScript 允許我們標記一個類為抽象類。這將告訴 TypeScript 該類只能被其他類擴展,并且擴展類必須包含確切的屬性才能實例化。
abstract class Shape { abstract getArea(): number; } // 錯誤! 不能實例化一個抽象類。 new Shape(); class Square extends Shape { #sideLength: number; constructor(sideLength: number) { this.#sideLengthsideLength = sideLength; } getArea() { return this.#sideLength ** 2; } } // 可以正確執行 new Square(42);
為了確保這條限制在新建抽象類的過程中始終有效,你不能將抽象類賦值給任何需要構造簽名的對象。
interface HasArea { getArea(): number; } // 錯誤!不能將抽象構造類型賦值給非抽象構造類型 let Ctor: new () => HasArea = Shape;
如果我們原意是執行 new Ctor 這樣的代碼,拋出異常是正確的行為。但如果我們是想編寫 Ctor 的子類,這種限制就顯得過于嚴格。
functon makeSubclassWithArea(Ctor: new () => HasArea) { return class extends Ctor { getArea() { // ... } } } let MyShape = makeSubclassWithArea(Shape);
同樣的,它同樣不能與內置的幫助類,例如 InstanceType,配合使用。
// 錯誤! // 類型 'typeof Shape' 不滿足于約束條件 'new (...args: any) => any'。 // 不能將抽象構造類型賦值給非抽象構造類型 type MyInstance = InstanceType<typeof Shape>;
這就是為什么 TypeScript 4.2 要允許你在構造簽名中指定抽象指示器。
interface HasArea { getArea(): number; } // 成功! let Ctor: abstract new () => HasArea = Shape; // ^^^^^^^^
為構造簽名增加抽象指示器,意味著你可以將其在抽象構造函數中傳遞。這不會阻止你向其傳遞其他的 “具象” 類或構造函數,該指示器實際上只是表示該類不會直接運行構造函數,因此可以安全地傳遞任何一種類型的類。
此項功能使我們可以用帶有抽象類的方式編寫 mixin 工廠函數。舉例來說,在下面這段代碼中,我們可以使用包含了抽象類 SuperClass 的 mixin 函數 withStyles。
abstract class SuperClass { abstract someMethod(): void; badda() {} } type AbstractConstructor<T> = abstract new (...args: any[]) => T function withStyles<T extends AbstractConstructor<object>>(Ctor: T) { abstract class StyledClass extends Ctor { getstyles() { // ... } } return StyledClass; } class SubClass extends withStyles(SuperClass) { someMethod() { this.someMethod() } }
請注意,withStyle 展示的是一條特定規則,必須將擴展自抽象構造函數(如 Ctor) 的類 (如 StyledClass) 也聲明為抽象類。這是因為無法知道是否傳入了具有更多抽象成員的類,并且也無法知道子類是否實現了所有抽象成員。
你可以在這個Pull Request[12]里,查看更多抽象構造簽名的內容。
對于 TypeScript 用戶來說,一個令人驚訝又常見的場景是被問到:“為什么 TypeScript 包含這個文件?” 推斷程序文件是一個復雜的過程,這就是為什么一個特定組合的 lib.d.ts 很有必要,為什么 node_modules 中特定的文件需要被包含以及為什么有些文件我們認為應該被排除但是結果卻被包含。
這也就是為什么 TypeScript 現在提供了 --explainFiles 標志。
tsc --explainFiles
當你啟用這個選項,TypeScript 編譯器將給出一些非常冗長的輸出,用于說明某個文件為什么會出現在程序里。為了方便的閱讀,你可以把輸出內容存入一個文件,或者通過管道輸出到更容易閱讀的程序中。
# 將輸出內容存入文件 tsc --explainFiles > expanation.txt # 通過管道將輸出內容發送到工具程序例如`less`,或者像 VS Code 這種編輯器 tsc --explainFiles | less tsc --explainFiles | code -
通常,輸出內容首先會列出包含 lib.d.ts 的原因,然后是本地文件,最后是 node_modules 文件。
TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts Library 'lib.esnext.d.ts' specified in compilerOptions ... More Library References... foo.ts Matched by include pattern '**/*' in 'tsconfig.json'
目前,我們還沒發保證輸出的格式 —— 在后續的更新中可能會不斷變化。值得一提的是,如果你對格式改進有任何好的建議,我們都非常感興趣。
更多信息,請參照原始 Pull Request[13]。
多虧了 Alex Tarasyuk[14] 的進一步改進,TypeScript 非執行函數檢查現在適用于 && 和 || 表達式。
現在在 --strictNullChecks模式下,以下代碼會報錯:
function shouldDisplayElement(element: Element) { // ... return true; } function getVisibleItems(elements: Element[]) { return elements.filter(e => shouldDisplayElement && e.children.length) // ~~~~~~~~~~~~~~~~~~~~ // 該條件始終會返回 true,因為該函數已經被定義了。 // 你是否是想執行它來代替? }
更多信息,詳見 Pull Request[15]。
感謝 Alex Tarasyuk[16] 的另一個 Pull Request,你現在可以通過在解構變量前加上下劃線,從而將其標記為未使用變量。
let [_first, second] = getValues();
之前的版本中,如果 _first 在之后的代碼中沒有被使用,那么 TypeScript 會拋出一個 noUnusedLocals 錯誤。現在,TypeScript 會認識到 _first 只聲明不調用是有意為之。
詳情可見這個 Pull Request [17]。
字符串索引簽名是用于鍵入類似字典對象的方式 —— 當你想允許該對象包含使用任意鍵名。
const movieWatchCount: { [key: string]: number } = {}; function watchMovie(title: string) { movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1; }
當然,現在該典中沒有包含任何電影標題,movieWatchCount[title] 將返回 undefined (TypeScript 4.1 中新增了一個選項 --noUncheckedIndexedAccess 會給字符串索引簽名自動添加 undefined 可選類型)。即使很明顯,movieWatchCount 必將在之后包含某些字符串,但由于存在 undefined,之前的版本的 TypeScript ,仍然會將可選對象屬性視為無法分配給其他兼容的索引簽名。
type WesAndersonWatchCount = { "Fantastic Mr. Fox"?: number; "The Royal Tenenbaums"?: number; "Moonrise Kingdom"?: number; "The Grand Budapest Hotel"?: number; }; declare const wesAndersonWatchCount: WesAndersonWatchCount; const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount; // ~~~~~~~~~~~~~~~ 錯誤! // 類型 'WesAndersonWatchCount' 不能賦值給 '{ [key: string]: number; }'. // 屬性 '"Fantastic Mr. Fox"' 和索引簽名不兼容。 // 類型 'number | undefined' 不能賦值給 'number'. // 類型 'undefined' 不能賦值給類型 'number'. (2322)
TypeScript 4.2 允許這個指派。但是,它不允許分配類型為 undefined 的非可選屬性,也不允許將 undefined 寫入特定鍵:
type BatmanWatchCount = { "Batman Begins": number | undefined; "The Dark Knight": number | undefined; "The Dark Knight Rises": number | undefined; }; declare const batmanWatchCount: BatmanWatchCount; // TypeScript 4.2. 依然會報錯 // `undefined` 指揮在屬性被標記為可選時才會被忽略。 const movieWatchCount: { [key: string]: number } = batmanWatchCount; // TypeScript 4.2. 依然會報錯 // 索引簽名不允許顯示定義為 `undefined`。 movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
新規則同樣不適用于數字索引簽名,因為它們被假定為類似數組的密集型數據結構。
你可以通過閱讀這個 Pull Request[18] 更好的理解這條規則。
感謝 Alex Tarasyuk[19] 實現的來自社區的 Pull Request[20],我們現在有了一個在聲明新函數和方法時的快速修復。
圖片
我們始終致力于將發布中的破壞性變化降至最低。TypeScript 4.2 包含一些破壞性變化,但我們認為它們在升級過程中時可控的。
如同每個版本的 TypeScript 升級,lib.d.ts 的聲明(特別是為 Web 上下文生成的聲明)都會發生改變。升級的變化有很多,但是最終可能最具有破壞性的是 Intl 和 ResizeObserver。
當捕獲了 yield 表達式的值,但是 TypeScript 不能立刻分辨出你打算接受的類型是哪種時(舉個例子:yield 表達式未按上下文類型輸入),TypeScript 現在會發出類型隱式聲明為 any 的錯誤。
function* g1() { const value = yield 1; // ~~~~~~~ // 錯誤! // 'yield' 表達式隱式返回 'any' 類型 // 因為其包含缺少返回類型注釋的生成器。 } function* g2() { // 正確。 // `yield 1` 的結果未被使用。 yield 1; } function* g3() { // 正確。 // `yield 1` 根據上下文被定義為 'string' 類型。 const value: string = yield 1; } function* g3(): Generator<number, void, string> { // 正確。 // 通過 'g3' 的顯式返回結果, // TypeScript 可以知道 'yield 1' 的類型 const value = yield 1; }
詳情可見這個 Pull Request[21]。
如上文所述,當啟用 --strictNullChecks 模式時,未執行函數檢查將會在 && 和 || 表達式中一致執行。這可能會成為一個潛在的破壞性,但通常表示現有代碼中存在邏輯錯誤。
JavaScript 中已經不允許使用類型參數,但是在 TypeScript 4.2 中,解析器將以更符合規范的方式解析它們。所以,當在 JavaScript 中編寫以下代碼時:
f<T>(100)
TypeScript 會把它解析為以下代碼:
(f < T) > (100)
當你利用 TypeScript 的 API 來解析 JavaScript 文件中的類型構造,你可能會感到困擾。
"foo" in 42 // ~~ // 錯誤!'in' 表達式右側不允許是基類型
詳細請參考這個 Pull Request[22]。
TypeScript 中元組類型可以由任何類型的擴展運算語法生成
// 由擴展元素生成的元組類型 type NumStr = [number, string]; type NumStrNumStr = [...NumStr, ...NumStr]; // 數組擴展運算 const numStr = [123, "hello"] as const; const numStrNumStr = [...numStr, ...numStr] as const;
有時,這些元組類型可能會意外的變得巨大,這會使類型檢查花費很長時間。為了防止類型檢查掛起(這在編輯器場景中尤為糟糕),TypeScript 使用一個限制器來避免這個情況的發生。
你可以在這個 Pull Request[23] 中查看詳情。
// 必須改為類似于以下類型: // - "./foo" // - "./foo.js" import { Foo } from "./foo.d.ts";
導入路徑應反映加載程序在運行時將執行的操作。以下幾種導入都是等價的:
import { Foo } from "./foo"; import { Foo } from "./foo.js"; import { Foo } from "./foo/index.js";
這個改變是從 TypeScript 4.2 Beta 版中刪除了一個功能。如果你還沒有升級到我們最新的一個穩定版,你可以不必關注這個。但是,也許你也有興趣簡單了解一下。
TypeScript 4.2 Beta 版包含了一個對模板字符串推斷的更改。在這個更改中,模板字符串字面或者被定義為給定的模板字符串類型或者簡化為多個字符串字面類型。這些類型當被賦值給變量時,都會被擴大為字符串類型。
declare const yourName: string; // 'bar' 是常量。 // 它擁有類型 '`hello ${string}`'. const bar = `hello ${yourName}`; // 'baz' 是變量 // 它擁有類型 'string'. let baz = `hello ${yourName}`;
這類似于字符串字面推斷的工作方式。
// 'bar' 的類型是 '"hello"'. const bar = "hello"; // 'baz' 的類型是 'string'. let baz = "hello";
因此,我們認為擁有字符串類型的模板字符串表達式應該為 “常量”。但是,從我們所見所得的情況來看,這并不總是可取的。
作為回應,我們恢復了這個功能(以及潛在的破壞性)。如果你確實想要給一個模板字符串表達式定義為字面量類型,你可以在它的后面增加 as const。
declare const yourName: string; // 'bar' 擁有類型 '`hello ${string}`'. const bar = `hello ${yourName}` as const; // ^^^^^^^^ // 'baz' 擁有類型 'string'. const baz = `hello ${yourName}`;
TypeScript 具有帶lift功能的 visitNode 函數。現在,lift 需要一個只讀的 Node[] 而非 NodeArray<Node>。
感謝各位的閱讀,以上就是“TypeScript 4.2有哪些新優點”的內容了,經過本文的學習后,相信大家對TypeScript 4.2有哪些新優點這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。