細數 TS 中那些奇怪的符號
本文將分享在學習 TypeScript 過程中,遇到的 10 大 “奇怪” 的符號。其中有一些符號,很多人第一次見的時候也覺得 “一臉懵逼”,希望本文對學習 TypeScript 的小伙伴能有一些幫助。
好的,下面我們來開始介紹。
一、! 非空斷言操作符
在上下文中當類型檢查器無法斷定類型時,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。
那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場景。
1.1 忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) {
const onlyString: string = maybeString;
const ignoreUndefinedAndNull: string = maybeString!;
}
1.2 調用函數時忽略 undefined 類型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
const num1 = numGenerator();
const num2 = numGenerator!();
}
因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除,所以在實際使用的過程中,要特別注意。比如下面這個例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
以上 TS 代碼會編譯生成以下 ES5 代碼:
"use strict";
const a = undefined;
const b = a;
console.log(b);
雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執(zhí)行以上代碼,在控制臺會輸出 undefined。
1.3 確定賦值斷言
在 TypeScript 2.7 版本中引入了確定賦值斷言,即允許在實例屬性和變量聲明后面放置一個 ! 號,從而告訴 TypeScript 該屬性會被明確地賦值。為了更好地理解它的作用,我們來看個具體的例子:
let x: number;
initialize();
console.log(2 * x);
function initialize() {
x = 10;
}
很明顯該異常信息是說變量 x 在賦值前被使用了,要解決該問題,我們可以使用確定賦值斷言:
let x!: number;
initialize();
console.log(2 * x);
function initialize() {
x = 10;
}
通過 let x!: number; 確定賦值斷言,TypeScript 編譯器就會知道該屬性會被明確地賦值。
二、?. 運算符
TypeScript 3.7 實現了呼聲最高的 ECMAScript 功能之一:可選鏈(Optional Chaining)。有了可選鏈后,我們編寫代碼時如果遇到 null 或 undefined 就可以立即停止某些表達式的運行??蛇x鏈的核心是新的 ?. 運算符,它支持以下語法:
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
這里我們來舉一個可選的屬性訪問的例子:
const val = a?.b;
為了更好的理解可選鏈,我們來看一下該 const val = a?.b 語句編譯生成的 ES5 代碼:
var val = a === null || a === void 0 ? void 0 : a.b;
上述的代碼會自動檢查對象 a 是否為 null 或 undefined,如果是的話就立即返回 undefined,這樣就可以立即停止某些表達式的運行。你可能已經想到可以使用 ?. 來替代很多使用 && 執(zhí)行空檢查的代碼:
if(a && a.b) { }
if(a?.b){ }
但需要注意的是,?. 與 && 運算符行為略有不同,&& 專門用于檢測 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只會驗證對象是否為 null 或 undefined,對于 0 或空字符串來說,并不會出現 “短路”。
2.1 可選元素訪問
可選鏈除了支持可選屬性的訪問之外,它還支持可選元素的訪問,它的行為類似于可選屬性的訪問,只是可選元素的訪問允許我們訪問非標識符的屬性,比如任意字符串、數字索引和 Symbol:
function tryGetArrayElement<T>(arr?: T[], index: number = 0) {
return arr?.[index];
}
以上代碼經過編譯后會生成以下 ES5 代碼:
"use strict";
function tryGetArrayElement(arr, index) {
if (index === void 0) { index = 0; }
return arr === null || arr === void 0 ? void 0 : arr[index];
}
通過觀察生成的 ES5 代碼,很明顯在 tryGetArrayElement 方法中會自動檢測輸入參數 arr 的值是否為 null 或 undefined,從而保證了我們代碼的健壯性。
2.2 可選鏈與函數調用
當嘗試調用一個可能不存在的方法時也可以使用可選鏈。在實際開發(fā)過程中,這是很有用的。系統中某個方法不可用,有可能是由于版本不一致或者用戶設備兼容性問題導致的。函數調用時如果被調用的方法不存在,使用可選鏈可以使表達式自動返回 undefined 而不是拋出一個異常。
可選調用使用起來也很簡單,比如:
let result = obj.customMethod?.();
該 TypeScript 代碼編譯生成的 ES5 代碼如下:
var result = (_a = obj.customMethod) === null
|| _a === void 0 ? void 0 : _a.call(obj);
另外在使用可選調用的時候,我們要注意以下兩個注意事項:
- 如果存在一個屬性名且該屬性名對應的值不是函數類型,使用
?.仍然會產生一個[TypeError](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypeError)異常。 - 可選鏈的運算行為被局限在屬性的訪問、調用以及元素的訪問 —— 它不會沿伸到后續(xù)的表達式中,也就是說可選調用不會阻止
a?.b / someMethod()表達式中的除法運算或someMethod的方法調用。
三、?? 空值合并運算符
在 TypeScript 3.7 版本中除了引入了前面介紹的可選鏈 ?. 之外,也引入了一個新的邏輯運算符 —— 空值合并運算符 ??。當左側操作數為 null 或 undefined 時,其返回右側的操作數,否則返回左側的操作數。
與邏輯或 || 運算符不同,邏輯或會在左操作數為 falsy 值時返回右側操作數。也就是說,如果你使用 || 來為某些變量設置默認的值時,你可能會遇到意料之外的行為。比如為 falsy 值(''、NaN 或 0)時。
這里來看一個具體的例子:
const foo = null ?? 'default string';
console.log(foo);
const baz = 0 ?? 42;
console.log(baz);
以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict";
var _a, _b;
var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo);
var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz);
通過觀察以上代碼,我們更加直觀的了解到,空值合并運算符是如何解決前面 || 運算符存在的潛在問題。下面我們來介紹空值合并運算符的特性和使用時的一些注意事項。
3.1 短路
當空值合并運算符的左表達式不為 null 或 undefined 時,不會對右表達式進行求值。
function A() { console.log('A was called'); return undefined;}
function B() { console.log('B was called'); return false;}
function C() { console.log('C was called'); return "foo";}
console.log(A() ?? C());
console.log(B() ?? C());
上述代碼運行后,控制臺會輸出以下結果:
A was called
C was called
foo
B was called
false
3.2 不能與 && 或 || 操作符共用
若空值合并運算符 ?? 直接與 AND(&&)和 OR(||)操作符組合使用 ?? 是不行的。這種情況下會拋出 SyntaxError。
null || undefined ?? "foo";
true && undefined ?? "foo";
但當使用括號來顯式表明優(yōu)先級時是可行的,比如:
(null || undefined ) ?? "foo";
3.3 與可選鏈操作符 ?. 的關系
空值合并運算符針對 undefined 與 null 這兩個值,可選鏈式操作符 ?. 也是如此??蛇x鏈式操作符,對于訪問屬性可能為 undefined 與 null 的對象時非常有用。
interface Customer {
name: string;
city?: string;
}
let customer: Customer = {
name: "Semlinker"
};
let customerCity = customer?.city ?? "Unknown city";
console.log(customerCity);
前面我們已經介紹了空值合并運算符的應用場景和使用時的一些注意事項,該運算符不僅可以在 TypeScript 3.7 以上版本中使用。當然你也可以在 JavaScript 的環(huán)境中使用它,但你需要借助 Babel,在 Babel 7.8.0 版本也開始支持空值合并運算符。
四、?: 可選屬性
在面向對象語言中,接口是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。 TypeScript 中的接口是一個非常靈活的概念,除了可用于對類的一部分行為進行抽象以外,也常用于對「對象的形狀(Shape)」進行描述。
在 TypeScript 中使用 interface 關鍵字就可以聲明一個接口:
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: "semlinker",
age: 33,
};
在以上代碼中,我們聲明了 Person 接口,它包含了兩個必填的屬性 name 和 age。在初始化 Person 類型變量時,如果缺少某個屬性,TypeScript 編譯器就會提示相應的錯誤信息,比如:
let lolo: Person = {
name: "lolo"
}
為了解決上述的問題,我們可以把某個屬性聲明為可選的:
interface Person {
name: string;
age?: number;
}
let lolo: Person = {
name: "lolo"
}
4.1 工具類型
4.1.1 Partial<T>
在實際項目開發(fā)過程中,為了提高代碼復用率,我們可以利用 TypeScript 內置的工具類型 Partial<T> 來快速把某個接口類型中定義的屬性變成可選的:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
}
type PullDownRefreshOptions = Partial<PullDownRefreshConfig>
是不是覺得 Partial<T> 很方便,下面讓我們來看一下它是如何實現的:
type Partial<T> = {
[P in keyof T]?: T[P];
};
4.1.2 Required<T>
既然可以快速地把某個接口中定義的屬性全部聲明為可選,那能不能把所有的可選的屬性變成必選的呢?答案是可以的,針對這個需求,我們可以使用 Required<T> 工具類型,具體的使用方式如下:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
}
type PullDownRefreshOptions = Partial<PullDownRefreshConfig>
type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>
同樣,我們來看一下 Required<T> 工具類型是如何實現的:
type Required<T> = {
[P in keyof T]-?: T[P];
};
原來在 Required<T> 工具類型內部,通過 -? 移除了可選屬性中的 ?,使得屬性從可選變?yōu)楸剡x的。
五、& 運算符
在 TypeScript 中交叉類型是將多個類型合并為一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
在上面代碼中我們先定義了 PartialPointX 類型,接著使用 & 運算符創(chuàng)建一個新的 Point 類型,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point 類型的變量并初始化。
5.1 同名基礎類型屬性的合并
那么現在問題來了,假設在合并多個類型的過程中,剛好出現某些類型存在相同的成員,但對應的類型又不一致,比如:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
在上面的代碼中,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致。對于這種情況,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 string 或 number 類型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };

q = { c: "c", d: "d", e: "e" };

為什么接口 X 和接口 Y 混入后,成員 c 的類型會變成 never 呢?這是因為混入后成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以混入后成員 c 的類型為 never。
5.2 同名非基礎類型屬性的合并
在上面示例中,剛好接口 X 和接口 Y 中內部成員 c 的類型都是基本數據類型,那么如果是非基本數據類型的話,又會是什么情形。我們來看個具體的例子:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};
console.log('abc:', abc);
以上代碼成功運行后,控制臺會輸出以下結果:

由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那么是可以成功合并。
六、| 分隔符
在 TypeScript 中聯合類型(Union Types)表示取值可以為多種類型中的一種,聯合類型使用 | 分隔每個類型。聯合類型通常與 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => { };
以上示例中 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給 sayHello 函數。
sayHello("semlinker");
sayHello(undefined);
此外,對于聯合類型來說,你可能會遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。
6.1 類型保護
當使用聯合類型時,我們必須盡量把當前值的類型收窄為當前值的實際類型,而類型保護就是實現類型收窄的一種手段。
類型保護是可執(zhí)行運行時檢查的一種表達式,用于確保該類型在一定的范圍內。換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數字。類型保護與特性檢測并不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。
目前主要有四種的方式來實現類型保護:
6.1.1 in 關鍵字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
6.1.2 typeof 關鍵字
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 類型保護只支持兩種形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必須是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。
6.1.3 instanceof 關鍵字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
}
6.1.4 自定義類型保護的類型謂詞(type predicate)
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
七、_ 數字分隔符
TypeScript 2.7 帶來了對數字分隔符的支持,正如數值分隔符 ECMAScript 提案中所概述的那樣。對于一個數字字面量,你現在可以通過把一個下劃線作為它們之間的分隔符來分組數字:
const inhabitantsOfMunich = 1_464_301;
const distanceEarthSunInKm = 149_600_000;
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
分隔符不會改變數值字面量的值,但邏輯分組使人們更容易一眼就能讀懂數字。以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict";
var inhabitantsOfMunich = 1464301;
var distanceEarthSunInKm = 149600000;
var fileSystemPermission = 504;
var bytes = 262926349;
7.1 使用限制
雖然數字分隔符看起來很簡單,但在使用時還是有一些限制。比如你只能在兩個數字之間添加 _ 分隔符。以下的使用方式是非法的:
3_.141592
3._141592
1_e10
1e_10
_126301
126301_
0_b111111000
0b_111111000
當然你也不能連續(xù)使用多個 _ 分隔符,比如:
123__456
7.2 解析分隔符
此外,需要注意的是以下用于解析數字的函數是不支持分隔符:
Number()parseInt()parseFloat()
這里我們來看一下實際的例子:
Number('123_456')
NaN
parseInt('123_456')
123
parseFloat('123_456')
123
很明顯對于以上的結果不是我們所期望的,所以在處理分隔符時要特別注意。當然要解決上述問題,也很簡單只需要非數字的字符刪掉即可。這里我們來定義一個 removeNonDigits 的函數:
const RE_NON_DIGIT = /[^0-9]/gu;
function removeNonDigits(str) {
str = str.replace(RE_NON_DIGIT, '');
return Number(str);
}
該函數通過調用字符串的 replace 方法來移除非數字的字符,具體的使用方式如下:
removeNonDigits('123_456')
123456
removeNonDigits('149,600,000')
149600000
removeNonDigits('1,407,836')
1407836
八、<Type> 語法
8.1 TypeScript 斷言
有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。通常這會發(fā)生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。
類型斷言有兩種形式: