keyof 和查詢型別 (Lookup Types)
在 JavaScript 中,經常會出現期望以屬性名作為引數的 API,但到目前為止,還無法表達這些 API 中所存在的型別關係。
引入索引型別查詢或 keyof;索引型別查詢 keyof T 會產生 T 的允許屬性名型別。keyof T 型別被視為 string 的子型別。
示例
tsinterface Person {name: string;age: number;location: string;}type K1 = keyof Person; // "name" | "age" | "location"type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...type K3 = keyof { [x: string]: Person }; // string
與其對應的概念是索引訪問型別,也稱為查詢型別 (lookup types)。在語法上,它們看起來完全像元素訪問,但卻作為型別書寫。
示例
tstype P1 = Person["name"]; // stringtype P2 = Person["name" | "age"]; // string | numbertype P3 = string["charAt"]; // (pos: number) => stringtype P4 = string[]["push"]; // (...items: string[]) => numbertype P5 = string[][0]; // string
您可以將此模式與型別系統的其他部分結合使用,以獲得型別安全的查詢。
tsfunction getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key]; // Inferred type is T[K]}function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) {obj[key] = value;}let x = { foo: 10, bar: "hello!" };let foo = getProperty(x, "foo"); // numberlet bar = getProperty(x, "bar"); // stringlet oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"setProperty(x, "foo", "string"); // Error!, string expected number
對映型別 (Mapped Types)
一個常見的任務是獲取一個現有型別,並使其每個屬性完全可選。假設我們有一個 Person 型別。
tsinterface Person {name: string;age: number;location: string;}
它的部分版本將是:
tsinterface PartialPerson {name?: string;age?: number;location?: string;}
使用對映型別,PartialPerson 可以作為對 Person 型別的通用轉換來編寫:
tstype Partial<T> = {[P in keyof T]?: T[P];};type PartialPerson = Partial<Person>;
對映型別是透過獲取字面量型別的並集,併為新的物件型別計算一組屬性而產生的。它們就像 Python 中的列表推導式,但它們產生的不是列表中的新元素,而是型別中的新屬性。
除了 Partial,對映型別還可以表示許多有用的型別轉換。
ts// Keep types the same, but make each property to be read-only.type Readonly<T> = {readonly [P in keyof T]: T[P];};// Same property names, but make the value a promise instead of a concrete onetype Deferred<T> = {[P in keyof T]: Promise<T[P]>;};// Wrap proxies around properties of Ttype Proxify<T> = {[P in keyof T]: { get(): T[P]; set(v: T[P]): void };};
Partial, Readonly, Record 和 Pick
如前所述,Partial 和 Readonly 是非常有用的結構。您可以使用它們來描述一些常見的 JS 例程,例如:
tsfunction assign<T>(obj: T, props: Partial<T>): void;function freeze<T>(obj: T): Readonly<T>;
因此,它們現在被預設包含在標準庫中。
我們還包含了另外兩種實用型別:Record 和 Pick。
ts// From T pick a set of properties Kdeclare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
ts// For every properties K of type T, transform it to Ufunction mapObject<K extends string, T, U>(obj: Record<K, T>,f: (x: T) => U): Record<K, U>;const names = { foo: "hello", bar: "world", baz: "bye" };const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
物件展開 (Spread) 與剩餘 (Rest)
TypeScript 2.1 帶來了對 ESnext 展開與剩餘操作的支援。
與陣列展開類似,展開物件可以方便地進行淺複製:
tslet copy = { ...original };
同樣,您可以合併多個不同的物件。在下面的示例中,merged 將擁有來自 foo、bar 和 baz 的屬性。
tslet merged = { ...foo, ...bar, ...baz };
您還可以覆蓋現有屬性並新增新屬性:
tslet obj = { x: 1, y: "string" };var newObj = { ...obj, z: 3, y: 4 }; // { x: number, y: number, z: number }
指定展開操作的順序決定了最終物件中包含哪些屬性;後續展開的屬性會“覆蓋”之前建立的屬性。
物件剩餘操作是物件展開的對偶,它們可以提取在解構元素時未被選中的任何額外屬性。
tslet obj = { x: 1, y: 1, z: 1 };let { z, ...obj1 } = obj;obj1; // {x: number, y:number};
向下相容非同步函式
此功能在 TypeScript 2.1 之前就已經得到支援,但僅限於目標環境為 ES6/ES2015 時。TypeScript 2.1 將此功能帶到了 ES3 和 ES5 執行時,這意味著無論您使用什麼環境,都可以自由地利用它。
注意:首先,我們需要確保我們的執行時具有全域性可用的、符合 ECMAScript 標準的
Promise。這可能涉及獲取一個Promise的 polyfill,或者依賴於您所目標執行時中可能已有的Promise。我們還需要透過將lib選項設定為類似"dom", "es2015"或"dom", "es2015.promise", "es5"的值,確保 TypeScript 知道Promise的存在。
示例
tsconfig.json
{"": {"": ["dom", "es2015.promise", "es5"]}}
dramaticWelcome.ts
tsfunction delay(milliseconds: number) {return new Promise<void>(resolve => {setTimeout(resolve, milliseconds);});}async function dramaticWelcome() {console.log("Hello");for (let i = 0; i < 3; i++) {await delay(500);console.log(".");}console.log("World!");}dramaticWelcome();
編譯並執行輸出程式碼應在 ES3/ES5 引擎上產生正確的行為。
支援外部輔助函式庫 (tslib)
TypeScript 會注入少量輔助函式,例如用於繼承的 __extends、用於物件字面量和 JSX 元素中展開運算子的 __assign,以及用於非同步函式的 __awaiter。
之前有兩種選擇:
- 在每個需要它們的檔案中注入輔助函式,或者
- 使用
noEmitHelpers完全不包含輔助函式。
這兩種選擇都無法滿足需求;在每個檔案中打包輔助函式對於試圖保持包體積小的客戶來說是一個痛點。而不包含輔助函式意味著客戶必須維護自己的輔助函式庫。
TypeScript 2.1 允許您在專案中將這些檔案作為一個獨立的模組包含一次,編譯器將根據需要為它們發出匯入語句。
首先,安裝 tslib 實用庫:
shnpm install tslib
其次,使用 importHelpers 編譯您的檔案:
shtsc --module commonjs --importHelpers a.ts
因此,給定以下輸入,生成的 .js 檔案將包含對 tslib 的匯入,並使用其中的 __assign 輔助函式,而不是將其內聯。
tsexport const o = { a: 1, name: "o" };export const copy = { ...o };
js"use strict";var tslib_1 = require("tslib");exports.o = { a: 1, name: "o" };exports.copy = tslib_1.__assign({}, exports.o);
無型別匯入 (Untyped imports)
一直以來,TypeScript 對如何匯入模組的要求非常嚴格。這是為了避免拼寫錯誤並防止使用者不正確地使用模組。
然而,很多時候,您可能只是想匯入一個現有的模組,該模組可能沒有自己的 .d.ts 檔案。以前這會被視為錯誤。從 TypeScript 2.1 開始,這變得容易多了。
使用 TypeScript 2.1,您可以匯入 JavaScript 模組而無需型別宣告。如果存在型別宣告(例如 declare module "foo" { ... } 或 node_modules/@types/foo),它仍然具有優先權。
在 noImplicitAny 下,對沒有宣告檔案的模組的匯入仍將被標記為錯誤。
示例
ts// Succeeds if `node_modules/asdf/index.js` existsimport { x } from "asdf";
支援 --target ES2016, --target ES2017 和 --target ESNext
TypeScript 2.1 支援三個新的目標值:--target ES2016, --target ES2017 和 --target ESNext。
使用 --target ES2016 將指示編譯器不要轉換 ES2016 特有的特性,例如 ** 運算子。
同樣,--target ES2017 將指示編譯器不要轉換 ES2017 特有的特性,如 async/await。
--target ESNext 針對的是最新支援的 ES 提案特性。
改進的 any 推斷
以前,如果 TypeScript 無法弄清楚變數的型別,它會選擇 any 型別。
tslet x; // implicitly 'any'let y = []; // implicitly 'any[]'let z: any; // explicitly 'any'.
從 TypeScript 2.1 開始,TypeScript 不再僅僅選擇 any,而是根據您後續分配的內容來推斷型別。
此功能僅在設定了 noImplicitAny 時啟用。
示例
tslet x;// You can still assign anything you want to 'x'.x = () => 42;// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.let y = x();// Thanks to that, it will now tell you that you can't add a number to a function!console.log(x + y);// ~~~~~// Error! Operator '+' cannot be applied to types '() => number' and 'number'.// TypeScript still allows you to assign anything you want to 'x'.x = "Hello world!";// But now it also knows that 'x' is a 'string'!x.toLowerCase();
現在也對空陣列執行了相同的跟蹤。
宣告時沒有型別註解且初始值為 [] 的變數被視為隱式 any[] 變數。但是,隨後的每個 x.push(value)、x.unshift(value) 或 x[n] = value 操作都會根據新增的元素演變變數的型別。
tsfunction f1() {let x = [];x.push(5);x[1] = "hello";x.unshift(true);return x; // (string | number | boolean)[]}function f2() {let x = null;if (cond()) {x = [];while (cond()) {x.push("hello");}}return x; // string[] | null}
隱式 any 錯誤
這帶來的一個巨大好處是,在使用 noImplicitAny 執行時,您會看到少得多的隱式 any 錯誤。隱式 any 錯誤僅在編譯器在沒有型別註解的情況下無法得知變數型別時報告。
示例
tsfunction f3() {let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.x.push(5);function g() {x; // Error: Variable 'x' implicitly has an 'any[]' type.}}
更好的字面量型別推斷
字串、數字和布林字面量型別(例如 "abc", 1 和 true)以前僅在存在顯式型別註解時才會推斷。從 TypeScript 2.1 開始,字面量型別始終會被推斷為 const 變數和 readonly 屬性。
對於沒有型別註解的 const 變數或 readonly 屬性,推斷出的型別是字面量初始值設定項的型別。對於帶有初始值設定項且沒有型別註解的 let 變數、var 變數、引數或非 readonly 屬性,推斷出的型別是初始值設定項的擴充套件字面量型別。其中字串字面量型別的擴充套件型別是 string,數字字面量型別是 number,true 或 false 是 boolean,列舉字面量型別是其所屬的列舉。
示例
tsconst c1 = 1; // Type 1const c2 = c1; // Type 1const c3 = "abc"; // Type "abc"const c4 = true; // Type trueconst c5 = cond ? 1 : "abc"; // Type 1 | "abc"let v1 = 1; // Type numberlet v2 = c2; // Type numberlet v3 = c3; // Type stringlet v4 = c4; // Type booleanlet v5 = c5; // Type number | string
字面量型別的擴充套件可以透過顯式型別註解進行控制。具體來說,當在沒有型別註解的 const 位置推斷出字面量型別的表示式時,該 const 變數會推斷出擴充套件的字面量型別。但當 const 位置有顯式的字面量型別註解時,該 const 變數會得到一個非擴充套件的字面量型別。
示例
tsconst c1 = "hello"; // Widening type "hello"let v1 = c1; // Type stringconst c2: "hello" = "hello"; // Type "hello"let v2 = c2; // Type "hello"
使用 super 呼叫返回的值作為 'this'
在 ES2015 中,返回物件的建構函式隱式地將 this 值替換為任何呼叫 super() 的物件。因此,必須捕獲 super() 的任何潛在返回值並將其替換為 this。此更改使得可以使用 Custom Elements,它利用這一點來使用使用者編寫的建構函式初始化瀏覽器分配的元素。
示例
tsclass Base {x: number;constructor() {// return a new object other than `this`return {x: 1};}}class Derived extends Base {constructor() {super();this.x = 2;}}
生成:
jsvar Derived = (function(_super) {__extends(Derived, _super);function Derived() {var _this = _super.call(this) || this;_this.x = 2;return _this;}return Derived;})(Base);
此更改會導致擴充套件內建類(如
Error,Array,Map等)的行為發生重大變化。請參閱擴充套件內建類破壞性更改文件以瞭解更多詳細資訊。
配置繼承
通常,一個專案有多個輸出目標,例如 ES5 和 ES2015、除錯和生產,或 CommonJS 和 System;在這兩個目標之間只有少數配置選項會發生變化,維護多個 tsconfig.json 檔案可能會很麻煩。
TypeScript 2.1 支援使用 extends 繼承配置,其中:
extends是tsconfig.json中的一個新頂級屬性(與compilerOptions、files、include和exclude並列)。extends的值必須是一個字串,包含要繼承的另一個配置檔案的路徑。- 基礎檔案的配置首先被載入,然後被繼承配置檔案中的配置所覆蓋。
- 不允許在配置檔案之間存在迴圈引用。
- 繼承配置檔案中的
files、include和exclude會覆蓋基礎配置檔案中的對應內容。 - 配置檔案中發現的所有相對路徑都將相對於它們所在的配置檔案進行解析。
示例
configs/base.json:
{"": {"": true,"": true}}
tsconfig.json:
{"": "./configs/base","": ["main.ts", "supplemental.ts"]}
tsconfig.nostrictnull.json:
{"": "./tsconfig","": {"": false}}
新標誌 --alwaysStrict
使用 alwaysStrict 呼叫編譯器會:
- 以嚴格模式解析所有程式碼。
- 在每個生成檔案的頂部寫入
"use strict";指令。
模組會自動在嚴格模式下解析。建議將此新標誌用於非模組程式碼。