Categories
程式開發

TypeScript 4.1 RC 版本發布:帶來了令人興奮的新特性


本月3日,微軟正式發布了TypeScript 4.1的發布候選(RC)版本。

需要安裝這個RC版的同學,可以通過NuGet“獲取,或使用npm命令:

npm install [email protected]

你還可以通過以下方式獲得編輯器支持:

下載Visual Studio 2019/2017按Visual Studio程式碼“和崇高文字“的指南操作。

在這個版本中我們提供了一些令人興奮的新特性、新的檢查標誌、編輯器生產力更新和性能改進。下面就來看看4.1為我們準備了哪些內容!

引入字符串模板類型在映射類型中加入鍵重映射遞歸條件類型新增檢查索引訪問功能–noUncheckedIndexedAccess使用path 啟用路徑映射時可以不指定baseUrlcheckJs 現在默認意味著allowJs,不再需要同時設置checkJs 和allowJs支持React 17 的JSX 功能JSDoc @see 標籤的編輯器支持重大更改

模板字面量(Template Literal)類型

我們可以使用TypeScript中的字符串字面量類型,來建模需要一組特定字符串的函數和API。

function setVerticalAlignment(color: "top" | "middle" | "bottom") {
// ...
}

setVerticalAlignment("middel");
// ~~~~~~~~
// error: Argument of type '"middel"' is not assignable to
// parameter of type '"top" | "middle" | "bottom"'.

這個特性很好用,因為字符串字面量類型可以對我們的字符串值進行基本的拼寫檢查。

另一個好處是,字符串字面量可以用作映射類型中的屬性名稱。從這個意義上講,它們也可用作構建塊。

type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

字符串字面量類型還可以用作另一種構建塊:構建其他字符串字面量類型。

所以TypeScript 4.1引入了模板字面量字符串類型。它的語法和JavaScript中的模板字面量字符串是一樣的,只是用在類型的場景中。當它用於字面量的具體類型(concrete type)時,它會串聯內容來生成一個新的字符串字面量類型。

type World = "world";

type Greeting = `hello ${World}`;
// same as
// type Greeting = "hello world";

在替代位置有聯合類型呢?它會生成可以由每個聯合成員表示的所有可能的字符串字面量的集合。

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";5

這個特性的用途遠不止發行說明里的這點小例子。例如,幾個用於UI組件的庫有一種在其API中同時指定垂直和水平對齊方式的方法,一般是用兩個分別表示橫縱軸對齊的字符串連接,例如“bottom- right”。垂直對齊可選的有“top”“middle”和“bottom”,水平對齊有“left”“center”和“right”,加起來有9個字符串選項,前後字符串之間都用破折號連接。

type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";

// Takes
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;

setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattl

雖然這類API可用的有很多,但我們可以手動把這些選項都寫出來,所以這個例子還是偏玩具一些的。實際上,如果只有9個字符串可選那沒什麼大不了。但當你需要大量字符串時,應考慮提前自動生成它們,這樣就用不著那麼多類型檢查了(或只使用string,這更容易理解)。

這個特性的一個很有價值的用途是自動態創建新的字符串字面量。例如,想像一個makeWatchedObject API,它接收一個對象並生成一個幾乎相同的對象,但加了一個新的on方法來檢測屬性的更改。

let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});

person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});

注意,on會偵聽事件“firstNameChanged”,而不僅僅是“firstName”。我們如何對其類型化呢?

type PropEventSource = {
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(obj: T): T & PropEventSource;

這樣,當我們賦予錯誤的屬性時,構建出的東西就會報錯!

// error!
person.on("firstName", () => {
});

// error!
person.on("frstNameChanged", () => {
});

我們還可以在模板字面量類型裡做一些特殊的事情:我們可以從替換位置做推斷。我們可以把最後一個示例通用化,從eventName字符串的各個部分做推斷,以找出關聯的屬性。

type PropEventSource = {
on
(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};

declare function makeWatchedObject(obj: T): T & PropEventSource;

let person = 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)。

推斷可以有多種組合方式,通常是解構字符串,並以多種方式對其進行重構。實際上,為了幫助大家修改這些字符串字面量類型,我們添加了一些新的實用程序類型別名,用於修改字母中的大小寫(也就是轉換為小寫和大寫字符)。

type EnthusiasticGreeting = `${Uppercase}`

type HELLO = EnthusiasticGreeting;
// same as
// type HELLO = "HELLO";

新的類型別名為Uppercase、Lowercase、Capitalize和Uncapitalize。前兩個會轉換字符串中的每個字符,後兩個僅轉換字符串中的第一個字符。

欲了解更多信息,請參見原始的拉取請求"和進行中的拉取請求“。

映射類型中加入鍵重映射

就像刷新器一樣,映射類型可以基於任意鍵創建新的對像類型:

type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

或基於其他對像類型創建新的對像類型:

/// 'Partial' is the same as 'T', but with each property marked optional.
type Partial = {
[K in keyof T]?: T[K]
};

以前,映射類型只能使用你提供的鍵來生成新的對像類型。但很多時候你希望能夠根據輸入來創建新鍵或過濾掉鍵。

因此,TypeScript 4.1允許你使用新的as子句重新映射映射類型中的鍵。

type MappedTypeWithNewKeys = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}

有了這個新的as子句,你可以利用模板字面量類型之類的特性,輕鬆地基於舊名稱創建屬性名稱。

type Getters = {
[K in keyof T as `get${Capitalize}`]: () => T[K]
};

interface Person {
name: string;
age: number;
location: string;
}

type LazyPerson = Getters

你甚至可以生成never來過濾掉密鑰。這意味著在某些情況下,你不必使用額外的Omit幫助程序類型。

// Remove the 'kind' property
type RemoveKindField = {
[K in keyof T as Exclude]: T[K]
};

interface Circle {
kind: "circle";
radius: number;
}

type KindlessCircle = RemoveKindField;
// same as
// type KindlessCircle = {
// radius: number;
// }

欲了解更多信息,請查看GitHub上的原始拉取請求“。

遞歸條件類型

在JavaScript中,經常能看到可以展開(flatten)並建立任意級別容器類型的函數。例如,考慮Promise實例上的.then()方法。 .then(...)一個個展開promise,直到它找到一個“不像promise”的值,然後將該值傳遞給一個回調。 Arrays上還有一個相對較新的flat方法,從中可以看出展開的深度能有多大。

以前,處於各種實際因素,在TypeScript的類型系統中無法表達這一點。儘管有一些破解方法可以實現它,但最後出來的類型看起來會很奇怪。

所以TypeScript 4.1放寬了對條件類型的一些限制——以便它們可以構建這些模式。在TypeScript 4.1中,條件類型現在可以立即在其分支中引用自身,這樣我們就更容易編寫遞歸類型別名了。

例如,如果我們想編寫一個類型來獲取嵌套數組的元素類型,則可以編寫以下deepFlatten類型。

type ElementType =
T extends ReadonlyArray ? ElementType : T;

function deepFlatten(x: T): ElementType[] {
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。

type Awaited = T extends PromiseLike ? Awaited : T;

/// Like `promise.then(...)`, but more accurate in types.
declare function customThen(
p: Promise,
onFulfilled: (value: Awaited) => U
): Promise>;

請記住,儘管這些遞歸類型都很強大,但使用它們的時候應該小心謹慎。

首先,這些類型可以完成很多工作,這意味著它們會增加類型檢查時間。用它計算Collat​​​​z猜想或斐波那契數列中的數字可能很有意思,但不要放在npm的.d.ts文件裡。

除了計算量大之外,這些類型還可能在足夠複雜的輸入上觸及內部遞歸深度上限。達到這一遞歸上限時將導致編譯時錯誤。一般來說最好不要使用這些類型,避免寫出一些在更實際的場景中會失敗的代碼。

實現細節見此“。

檢查索引訪問

TypeScript有一個稱為索引簽名的特性。這些簽名可以用來告知類型系統,用戶可以訪問任意命名的屬性。

interface Options {
path: string;
permissions: number;

// Extra properties are caught by this index signature.
[propName: string]: string | number;
}

function checkOptions(opts: Options) {
opts.path // string
opts.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有一個索引簽名,其含義是任何尚未列出的accessed屬性都應具有string | number類型。理想情況下(代碼假定你知道自己在幹什麼)這很方便,但事實是,JavaScript中的大多數值並不能完整支持所有潛在的屬性名稱。例如,大多數類型都不會像前面的示例那樣,有一個Math.random()創建的屬性鍵的值。對於許多用戶而言,這種行為是超乎預料的,並且會感覺它沒有充分利用--strictNullChecks的嚴格檢查。

因此,TypeScript 4.1加入了一個名為--noUncheckedIndexedAccess的新標誌。在這種新模式下,每個屬性訪問(如foo.bar)或索引訪問(如foo["bar"])都被認為可能是undefined的。這意味著在我們的最後一個示例中,opts.yadda的類型為string | number | undefined,而不只是string | number。如果你需要訪問該屬性,則必須先檢查其是否存在,或者使用非null斷言運算符(後綴! 字符)。

// 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的一個後果是,即使在邊界檢查循環中,也會更嚴格地檢查對數組的索引。

function screamLines(strs: string[]) {
// this will have issues
for (let i = 0; i < strs.length; i++) { console.log(strs[i].toUpperCase()); // ~~~~~~~ // error! Object is possibly 'undefined'. } }

如果不需要索引,則可以使用for–of循環或forEach調用來遍歷各個元素。

function screamLines(strs: string[]) {
// this works fine
for (const str of strs) {
console.log(str.toUpperCase());
}

// this works fine
strs.forEach(str => {
console.log(str.toUpperCase());
});
}

捕獲越界錯誤時這個標誌可能很方便,但它對於很多代碼來說可能顯得很累贅,因此--strict標誌不會自動啟用它。但如果你對這個特性很感興趣,也可以隨意嘗試它,看它是否適合你團隊的代碼庫!

欲了解更多信息,請查看實現的拉取請求“。

沒有baseUrl的paths

路徑映射是相當常用的,通常是為了更好地導入,或者為了模擬monorepo鏈接行為。

不幸的是,指定paths來啟用路徑映射時,還需要指定一個名為baseUrl的選項,該選項也允許到達相對於baseUrl的bare specifier paths。它還經常會使自動導入使用較差的路徑。

在TypeScript 4.1中,可以在沒有baseUrl的情況下使用path選項,從而避免其中一些問題。

checkJs隱含allowJs

以前,如果你要啟動一個checked的JavaScript項目,則必須同時設置allowJs和checkJs。這有點煩人,因此現在checkJs默認隱含了allowJs。

欲了解更多信息,請查看拉取請求“。

React 17 JSX工廠

TypeScript 4.1通過jsx編譯器選項的兩個新選項,支持了React 17即將推出的jsx和jsxs工廠函數:

react-jsxreact-jsxdev

這些選項分別用於生產和開發編譯環境。一般來說,一個選項可以從另一個擴展而來。例如,用於生產構建的tsconfig.json可能如下所示:

// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}

用於開發的構建可能如下所示:

// ./src/tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}

欲了解更多信息,請查看相應的公關“。

JSDoc @see標籤的編輯器支持

JSDoc @see標籤現在在TypeScript和JavaScript的編輯器中得到了更好的支持。這樣你就可以在標籤後的虛線名稱中使用go-to-definition之類的功能。例如,在下面的示例中,僅對JSDoc註釋中的first或C進行go-to-defintion即可:

// @filename: first.ts
export class C { }

// @filename: main.ts
import * as first from './first';

/**
* @see first.C
*/
function related() {

感謝積極貢獻者Wenlu Wang 實現它!

重大更改

abstract成員不能被標記為async

標記為abstract的成員不能再標記為async。此處的解決方法是移除async關鍵字,因為調用方只關心返回類型。

any/unknown在falsy位置傳播

以前,對於像foo && somethingElse這樣的表達式,foo的類型是any或unknown的,整個表達式的類型將是somethingElse的類型。

例如,以前在下列代碼中x的類型為{ someProp: string }。

declare let foo: unknown;
declare let somethingElse: { someProp: string };

let x = foo && somethingElse;

但在TypeScript 4.1中,我們會更謹慎地確定這種類型。由於對&&左側的類型一無所知,因此我們將向外傳播any和unknown,而不是將右側的類型傳播出去。

它最常見的使用模式出現在檢查booleans的兼容性時,尤其是在謂詞函數中。

function isThing(x: any): boolean {
return x && typeof x === 'object' && x.blah === 'foo';
}

一般來說,合適的解決方法是從foo && someExpression切換到!!foo && someExpression。

條件spread創建可選屬性

在JavaScript中,對象spread(例如{ ...foo })不會對虛假值起作用。因此,在類似{ ...foo }的代碼中,如果foo為null或undefined,則會跳過foo。

許多用戶利用此優勢“有條件地”在屬性中spread。

interface 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的屬性將被spread進去;否則,不會將任何屬性spread到返回的對像中。

copyOwner的返回類型以前是基於每個spread的聯合類型:

{ x: number } | { x: number, name: string, age: number, location: string }

這個操作是這樣的:如果定義了pet,Person的所有屬性都將存在;否則,所有屬性都不會在結果上定義。要么全有,要么都沒有。

但有人把這種模式用得太過分了,在單個對像中塞幾百個spread,每個spread都可能添加數百或數千個屬性。事實證明,由於各種原因,這種做法的成本最後會飛天,並且往往不會帶來太多收益。

在TypeScript 4.1中,返回的類型改為使用all-optional屬性。

{
x: number;
name?: string;
age?: number;
location?: string;
}

這樣性能和代碼簡潔程度都會上一個台階。

欲了解更多信息,請參見原始更改(https://github.com/microsoft/TypeScript/pull/40778“)。

--declaration和--outFile需要包名稱根

當你有一個同時使用outFile和declaration,來為你的項目發出單個.js文件以及相應的.d.ts文件的項目時,該聲明文件通常需要對模塊標識符進行某種後處理,才能對外部消費者有意義。例如,像這樣的項目:

// @filename: projectRoot/index.ts
export * from "./nested/base";

// @filename: projectRoot/nested/base.ts
export const a = "123"

將生成一個如下所示的.d.ts文件:

declare module "nested/base" {
export const a = "123";
}
declare module "index" {
export * from "nested/base";
}

從技術上講這是準確的,但沒那麼有用。當請求生成單個.d.ts文件時,TypeScript 4.1會要求指定bundledPackageName。

declare module "hello/nested/base" {
export const a = "123";
}
declare module "hello" {
export * from "hello/nested/base";
}

沒有這個選項的話,你可能會收到像下面這樣的錯誤消息:

The `bundledPackageName` option must be provided when using outFile and node module resolution with declaration emit.

在Promise中,resolve的參數不再可選

在編寫如下代碼時:

new Promise(resolve => {
doSomethingAsync(() => {
doSomething();
resolve();
})
})

你可能會收到這樣的錯誤:

resolve()
~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
An argument for 'value' was not provided.

這是因為resolve不再具有可選參數,因此默認情況下現在必須為它傳遞一個值。一般來說,使用Promise時這樣會捕獲合法錯誤。典型的解決方法是為其傳遞正確的參數,有時還要添加一個顯式的類型參數。

new Promise(resolve => {
// ^^^^^^^^
doSomethingAsync(value => {
doSomething();
resolve(value);
// ^^^^^
})
})

但有時確實需要在沒有參數的情況下調用resolve()。在這些情況下,我們可以給Promise一個顯式的void泛型類型參數(即將其寫為Promise)。這利用了TypeScript 4.1中的新功能,其中可能是void的尾隨參數可以變為可選。

new Promise(resolve => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
})
})

TypeScript 4.1附帶了一個快速修復以幫助解決這個問題。

下一步計劃

在接下來的幾週內,我們將密切注意TypeScript 4.1的穩定版本中需要包含的所有高優先級修復。如果可以的話,請試試我們的RC版本,幫助我們找出各種潛在問題。我們一直在努力改善大家的TypeScript體驗!

編程快樂!

原文鏈接:

https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-rc/#breaking-changes