模板字面量型別
TypeScript 中的字串字面量型別允許我們對期望一組特定字串的函式和 API 進行建模。
tsTryfunctionsetVerticalAlignment (location : "top" | "middle" | "bottom") {// ...}Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.2345Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.setVerticalAlignment ("middel" );
這非常不錯,因為字串字面量型別基本上可以對我們的字串值進行拼寫檢查。
我們還喜歡將字串字面量用作對映型別中的屬性名。從這個意義上說,它們也可以用作構建塊。
tstype Options = {[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;};// same as// type Options = {// noImplicitAny?: boolean,// strictNullChecks?: boolean,// strictFunctionTypes?: boolean// };
但字串字面量型別還可以作為另一個領域的構建塊:構建其他字串字面量型別。
這就是 TypeScript 4.1 引入模板字面量字串型別的原因。它具有與 JavaScript 中的模板字面量字串相同的語法,但用於型別位置。當你將其與具體的字面量型別一起使用時,它會透過拼接內容生成一個新的字串字面量型別。
tsTrytypeWorld = "world";typeGreeting = `hello ${World }`;
當替換位置有聯合型別時會發生什麼?它會產生由每個聯合成員所能表示的所有可能的字串字面量的集合。
tsTrytypeColor = "red" | "blue";typeQuantity = "one" | "two";typeSeussFish = `${Quantity |Color } fish`;
這不僅僅可以用於發行說明中的簡單示例。例如,一些 UI 元件庫可以透過其 API 指定垂直和水平對齊方式,通常使用像 "bottom-right" 這樣的單個字元串同時指定兩者。在垂直對齊 "top"、"middle" 和 "bottom" 以及水平對齊 "left"、"center" 和 "right" 之間,有 9 種可能的字串,其中每個前置字串都透過連字元與每個後置字串相連。
tsTrytypeVerticalAlignment = "top" | "middle" | "bottom";typeHorizontalAlignment = "left" | "center" | "right";// Takes// | "top-left" | "top-center" | "top-right"// | "middle-left" | "middle-center" | "middle-right"// | "bottom-left" | "bottom-center" | "bottom-right"declare functionsetAlignment (value : `${VerticalAlignment }-${HorizontalAlignment }`): void;setAlignment ("top-left"); // works!Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.setAlignment ("top-middel" ); // error!Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.setAlignment ("top-pot" ); // error! but good doughnuts if you're ever in Seattle
雖然這類 API 在實際應用中很多,但這仍然只是一個玩具示例,因為我們可以手動寫出這些組合。事實上,對於 9 個字串來說這可能沒問題;但當你需要大量字串時,你應該考慮提前自動生成它們,以節省每次型別檢查的工作量(或者乾脆使用 string,這樣更容易理解)。
一些真正的價值來自於動態建立新的字串字面量。例如,想象一個 makeWatchedObject API,它接收一個物件並生成一個基本相同的物件,但帶有一個新的 on 方法來檢測屬性的變化。
tslet person = makeWatchedObject({firstName: "Homer",age: 42, // give-or-takelocation: "Springfield",});person.on("firstNameChanged", () => {console.log(`firstName was changed!`);});
請注意,on 監聽的是事件 "firstNameChanged",而不僅僅是 "firstName"。我們該如何為它編寫型別呢?
tstype PropEventSource<T> = {on(eventName: `${string & keyof T}Changed`, callback: () => void): void;};/// Create a "watched object" with an 'on' method/// so that you can watch for changes to properties.declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
有了這個,我們就可以構建出一種在我們給出錯誤屬性時報錯的東西!
tsTry// error!Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.person .on ("firstName" , () => {});// error!Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.person .on ("frstNameChanged" , () => {});
我們還可以在模板字面量型別中做一些特別的事情:我們可以從替換位置推斷型別。我們可以使上一個示例通用化,從 eventName 字串的部分中推斷出關聯的屬性。
tsTrytypePropEventSource <T > = {on <K extends string & keyofT >(eventName : `${K }Changed`,callback : (newValue :T [K ]) => void ): void;};declare functionmakeWatchedObject <T >(obj :T ):T &PropEventSource <T >;letperson =makeWatchedObject ({firstName : "Homer",age : 42,location : "Springfield",});// works! 'newName' is typed as 'string'person .on ("firstNameChanged",newName => {// 'newName' has the type of 'firstName'console .log (`new name is ${newName .toUpperCase ()}`);});// works! 'newAge' is typed as 'number'person .on ("ageChanged",newAge => {if (newAge < 0) {console .log ("warning! negative age");}})
在這裡,我們將 on 變成了一個泛型方法。當用戶使用字串 "firstNameChanged" 呼叫時,TypeScript 會嘗試為 K 推斷出正確的型別。為了做到這一點,它會將 K 與 "Changed" 之前的內容進行匹配,並推斷出字串 "firstName"。一旦 TypeScript 確定了這一點,on 方法就可以獲取原始物件上 firstName 的型別,在本例中為 string。同樣,當我們用 "ageChanged" 呼叫時,它會找到屬性 age 的型別,即 number。
推斷可以以不同的方式組合,通常用於解構字串,並以不同的方式重構它們。事實上,為了幫助修改這些字串字面量型別,我們添加了一些新的工具類型別名,用於修改字母的大小寫(即轉換為小寫和大寫字元)。
tsTrytypeEnthusiasticGreeting <T extends string> = `${Uppercase <T >}`typeHELLO =EnthusiasticGreeting <"hello">;
新的類型別名是 Uppercase、Lowercase、Capitalize 和 Uncapitalize。前兩個轉換字串中的每個字元,後兩個僅轉換字串中的第一個字元。
更多詳細資訊,請檢視原始拉取請求和正在進行的切換到類型別名助手的拉取請求。
對映型別中的鍵重對映
作為複習,對映型別可以基於任意鍵建立新的物件型別
tstype Options = {[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;};// same as// type Options = {// noImplicitAny?: boolean,// strictNullChecks?: boolean,// strictFunctionTypes?: boolean// };
或者基於其他物件型別建立新的物件型別。
ts/// 'Partial<T>' is the same as 'T', but with each property marked optional.type Partial<T> = {[K in keyof T]?: T[K];};
到目前為止,對映型別只能產生具有你提供給它們的鍵的新物件型別;然而,很多時候你希望能夠基於輸入來建立新鍵或過濾掉某些鍵。
這就是為什麼 TypeScript 4.1 允許你透過新的 as 子句在對映型別中重新對映鍵。
tstype MappedTypeWithNewKeys<T> = {[K in keyof T as NewKeyType]: T[K]// ^^^^^^^^^^^^^// This is the new syntax!}
有了這個新的 as 子句,你可以利用模板字面量型別等特性,輕鬆地基於舊屬性名建立新的屬性名。
tsTrytypeGetters <T > = {[K in keyofT as `get${Capitalize <string &K >}`]: () =>T [K ]};interfacePerson {name : string;age : number;location : string;}typeLazyPerson =Getters <Person >;
你甚至可以透過產生 never 來過濾掉鍵。這意味著在某些情況下,你不必使用額外的 Omit 輔助型別。
tsTry// Remove the 'kind' propertytypeRemoveKindField <T > = {[K in keyofT asExclude <K , "kind">]:T [K ]};interfaceCircle {kind : "circle";radius : number;}typeKindlessCircle =RemoveKindField <Circle >;
欲瞭解更多資訊,請檢視 GitHub 上的原始拉取請求。
遞迴條件型別
在 JavaScript 中,看到能夠以任意級別展平並構建容器型別的函式是很常見的。例如,考慮 Promise 例項上的 .then() 方法。.then(...) 會解開每個 Promise,直到找到一個“非 Promise”的值,並將該值傳遞給回撥。在 Array 上還有一個相對較新的 flat 方法,它可以接受一個深度引數來決定展平的深度。
在 TypeScript 的型別系統中表達這一點,在所有實際意義上都是不可能的。雖然有一些 hack 方法可以實現這一點,但最終的型別看起來非常不合理。
這就是 TypeScript 4.1 放寬了一些條件型別限制的原因——以便它們可以對這些模式進行建模。在 TypeScript 4.1 中,條件型別現在可以在其分支內立即引用自身,從而更容易編寫遞迴類型別名。
例如,如果我們想編寫一個獲取巢狀陣列元素型別的型別,我們可以編寫以下 deepFlatten 型別。
tstype ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {throw "not implemented";}// All of these return the type 'number[]':deepFlatten([1, 2, 3]);deepFlatten([[1], [2, 3]]);deepFlatten([[1], [[2]], [[[3]]]]);
同樣,在 TypeScript 4.1 中,我們可以編寫一個 Awaited 型別來深度解開 Promise。
tstype Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;/// Like `promise.then(...)`, but more accurate in types.declare function customThen<T, U>(p: Promise<T>,onFulfilled: (value: Awaited<T>) => U): Promise<Awaited<U>>;
請記住,雖然這些遞迴型別很強大,但應負責任且節制地使用它們。
首先,這些型別可能會執行大量工作,這意味著它們會增加型別檢查時間。嘗試在 Collatz 猜想或斐波那契序列中模擬數字可能很有趣,但不要將這些內容釋出在 npm 的 .d.ts 檔案中。
除了計算密集之外,這些型別在處理足夠複雜的輸入時可能會達到內部遞迴深度限制。當達到該遞迴限制時,會導致編譯時錯誤。總的來說,最好完全不使用這些型別,而不是編寫在更現實的示例中失敗的程式碼。
檢視更多實現詳情。
檢查索引訪問 (--noUncheckedIndexedAccess)
TypeScript 有一個稱為索引簽名的特性。這些簽名是一種向型別系統發出訊號的方式,即使用者可以訪問任意命名的屬性。
tsTryinterfaceOptions {path : string;permissions : number;// Extra properties are caught by this index signature.[propName : string]: string | number;}functioncheckOptions (opts :Options ) {opts .path ; // stringopts .permissions ; // number// These are all allowed too!// They have the type 'string | number'.opts .yadda .toString ();opts ["foo bar baz"].toString ();opts [Math .random ()].toString ();}
在上面的例子中,Options 有一個索引簽名,表示任何未列出的已訪問屬性都應具有 string | number 型別。這通常對於那些假設你知道自己在做什麼的樂觀程式碼來說很方便,但事實是 JavaScript 中的大多數值並不支援每個潛在的屬性名。例如,大多數型別不會擁有像前一個例子中由 Math.random() 建立的屬性鍵的值。對於許多使用者來說,這種行為是不可取的,並且感覺沒有充分利用 strictNullChecks 的嚴格檢查。
這就是為什麼 TypeScript 4.1 引入了一個名為 noUncheckedIndexedAccess 的新標誌。在這種新模式下,每個屬性訪問(如 foo.bar)或索引訪問(如 foo["bar"])都被認為是可能未定義的。這意味著在我們的最後一個例子中,opts.yadda 的型別將是 string | number | undefined,而不是僅僅是 string | number。如果你需要訪問該屬性,你要麼必須先檢查它的存在性,要麼使用非空斷言運算子(字尾 ! 字元)。
tsTryfunctioncheckOptions (opts :Options ) {opts .path ; // stringopts .permissions ; // number// These are not allowed with noUncheckedIndexedAccess'opts.yadda' is possibly 'undefined'.18048'opts.yadda' is possibly 'undefined'.opts .yadda .toString ();Object is possibly 'undefined'.2532Object is possibly 'undefined'.opts ["foo bar baz"].toString ();Object is possibly 'undefined'.2532Object is possibly 'undefined'.opts [Math .random ()].toString ();// Checking if it's really there first.if (opts .yadda ) {console .log (opts .yadda .toString ());}// Basically saying "trust me I know what I'm doing"// with the '!' non-null assertion operator.opts .yadda !.toString ();}
使用 noUncheckedIndexedAccess 的一個後果是,即使在邊界檢查的迴圈中,對陣列的索引也會受到更嚴格的檢查。
tsTryfunctionscreamLines (strs : string[]) {// This will have issuesfor (leti = 0;i <strs .length ;i ++) {Object is possibly 'undefined'.2532Object is possibly 'undefined'.console .log (strs [i ].toUpperCase ());}}
如果你不需要索引,可以使用 for-of 迴圈或 forEach 呼叫來遍歷單個元素。
tsTryfunctionscreamLines (strs : string[]) {// This works finefor (conststr ofstrs ) {console .log (str .toUpperCase ());}// This works finestrs .forEach ((str ) => {console .log (str .toUpperCase ());});}
此標誌對於捕獲越界錯誤很有用,但對於許多程式碼來說可能過於吵鬧,因此它不會被 strict 標誌自動啟用;但是,如果你對這個特性感興趣,請隨時嘗試,並確定它是否適合你團隊的程式碼庫!
你可以在實現此功能的拉取請求中瞭解更多資訊。
沒有 baseUrl 的 paths
使用路徑對映非常普遍——通常是為了有更好的匯入體驗,或者模擬 monorepo 的連結行為。
不幸的是,指定 paths 來啟用路徑對映還需要指定一個名為 baseUrl 的選項,這也會允許裸說明符路徑相對於 baseUrl 被解析。這通常還會導致自動匯入使用不佳的路徑。
在 TypeScript 4.1 中,paths 選項可以在沒有 baseUrl 的情況下使用。這有助於避免其中的一些問題。
checkJs 隱含 allowJs
以前,如果你要啟動一個經過檢查的 JavaScript 專案,你必須同時設定 allowJs 和 checkJs。這稍微有些煩人,所以 checkJs 現在預設隱含了 allowJs。
React 17 JSX 工廠
TypeScript 4.1 透過 jsx 編譯器選項的兩個新選項支援 React 17 即將推出的 jsx 和 jsxs 工廠函式
react-jsxreact-jsxdev
這些選項分別用於生產和開發編譯。通常,其中一個選項可以繼承另一個。例如,用於生產構建的 tsconfig.json 可能如下所示
// ./src/tsconfig.json{"": {"": "esnext","": "es2015","": "react-jsx","": true},"": ["./**/*"]}
而用於開發構建的可能如下所示
// ./src/tsconfig.dev.json{"": "./tsconfig.json","": {"": "react-jsxdev"}}
欲瞭解更多資訊,請檢視相應的 PR。
JSDoc @see 標籤的編輯器支援
JSDoc @see 標籤現在在 TypeScript 和 JavaScript 編輯器中獲得了更好的支援。這允許你在標籤後的點號名稱上使用諸如“轉到定義”之類的功能。例如,在以下示例中,轉到 JSDoc 註釋中的 first 或 C 的定義均可正常工作
ts// @filename: first.tsexport class C {}// @filename: main.tsimport * as first from "./first";/*** @see first.C*/function related() {}
感謝頻繁貢獻者 Wenlu Wang 實現此功能!
破壞性變更
lib.d.ts 變更
lib.d.ts 可能有一組 API 變更,這可能部分歸因於 DOM 型別自動生成的方式。一個具體的改變是 Reflect.enumerate 已被移除,因為它已在 ES2016 中被移除。
abstract 成員不能標記為 async
標記為 abstract 的成員不能再標記為 async。這裡的修復方法是移除 async 關鍵字,因為呼叫者只關心返回型別。
any/unknown 在假值位置被傳播
以前,對於像 foo && somethingElse 這樣的表示式,如果 foo 的型別是 any 或 unknown,則整個表示式的型別將是 somethingElse 的型別。
例如,之前 x 的型別是 { someProp: string }。
tsdeclare let foo: unknown;declare let somethingElse: { someProp: string };let x = foo && somethingElse;
然而,在 TypeScript 4.1 中,我們更加謹慎地確定這種型別。由於 && 左側的型別未知,我們將 any 和 unknown 向外傳播,而不是右側的型別。
我們看到的最常見的模式傾向於在檢查與 boolean 的相容性時發生,特別是在謂詞函式中。
tsfunction isThing(x: any): boolean {return x && typeof x === "object" && x.blah === "foo";}
通常,合適的修復方法是將 foo && someExpression 切換為 !!foo && someExpression。
resolve 的引數在 Promise 中不再是可選的
當編寫如下程式碼時
tsnew Promise((resolve) => {doSomethingAsync(() => {doSomething();resolve();});});
你可能會遇到類似以下的錯誤
resolve()~~~~~~~~~error TS2554: Expected 1 arguments, but got 0.An argument for 'value' was not provided.
這是因為 resolve 不再有可選引數,因此預設情況下,現在必須傳遞一個值。這通常可以捕獲使用 Promise 時的合法錯誤。典型的修復方法是傳遞正確的引數,有時還需要新增顯式的型別引數。
tsnew Promise<number>((resolve) => {// ^^^^^^^^doSomethingAsync((value) => {doSomething();resolve(value);// ^^^^^});});
然而,有時 resolve() 確實需要不帶引數地呼叫。在這些情況下,我們可以給 Promise 一個顯式的 void 泛型型別引數(即寫成 Promise<void>)。這利用了 TypeScript 4.1 中的新功能,即潛在的 void 尾隨引數可以變為可選。
tsnew Promise<void>((resolve) => {// ^^^^^^doSomethingAsync(() => {doSomething();resolve();});});
TypeScript 4.1 附帶了一個快速修復程式來幫助解決這個破壞性更改。
條件傳播建立可選屬性
在 JavaScript 中,物件傳播(如 { ...foo })不對假值進行操作。因此,在像 { ...foo } 這樣的程式碼中,如果 foo 是 null 或 undefined,它將被跳過。
許多使用者利用這一點來“有條件地”傳播屬性。
tsinterface Person {name: string;age: number;location: string;}interface Animal {name: string;owner: Person;}function copyOwner(pet?: Animal) {return {...(pet && pet.owner),otherStuff: 123,};}// We could also use optional chaining here:function copyOwner(pet?: Animal) {return {...pet?.owner,otherStuff: 123,};}
在這裡,如果 pet 被定義,pet.owner 的屬性將被傳播進來——否則,返回的物件中將不會傳播任何屬性。
copyOwner 的返回型別以前是基於每次傳播的聯合型別
{ x: number } | { x: number, name: string, age: number, location: string }
這準確地模擬了操作發生的方式:如果 pet 被定義,那麼 Person 中的所有屬性都將存在;否則,它們都不會在結果中定義。這是一個全有或全無的操作。
然而,我們已經看到這種模式被推向了極端,單個物件中有數百次傳播,每次傳播都可能增加數百或數千個屬性。事實證明,由於各種原因,這最終變得極其昂貴,而且通常沒有什麼好處。
在 TypeScript 4.1 中,返回的型別有時會使用全可選屬性。
{x: number;name?: string;age?: number;location?: string;}
這最終表現更好,而且顯示效果也更好。
更多詳情,請參閱原始更改。雖然此行為目前並不完全一致,但我們預計未來的版本將產生更清晰和更可預測的結果。
不匹配的引數不再相關
TypeScript 以前會將不對應的引數透過關聯到 any 型別來聯絡起來。隨著 TypeScript 4.1 中的更改,語言現在完全跳過了這個過程。這意味著某些可賦值性的情況現在會失敗,但也意味著某些過載解析的情況也會失敗。例如,Node.js 中 util.promisify 的過載解析在 TypeScript 4.1 中可能會選擇不同的過載,有時會導致下游出現新的或不同的錯誤。
作為變通方法,你最好使用型別斷言來壓制錯誤。