類型別名#
抽離一組聯合類型:
type StatusCode = 200 | 301 | 400 | 500 | 502;
type PossibleDataTypes = string | number | (() => unknown);
const status: StatusCode = 502;
抽離一個函數類型:
type Handler = (e: Event) => void;
const clickHandler: Handler = (e) => { };
const moveHandler: Handler = (e) => { };
const dragHandler: Handler = (e) => { };
在類型別名中,類型別名可以這麼聲明自己能夠接受泛型(我稱之為泛型坑位)。一旦接受了泛型,我們就叫它工具類型:
type MaybeNull<T> = T | null;
function process(input: MaybeNull<{ handler: () => {} }>) {
input?.handler();
}
type MaybeArray<T> = T | T[];
// 函數泛型我們會在後面了解~
function ensureArray<T>(input: MaybeArray<T>): T[] {
return Array.isArray(input) ? input : [input];
}
聯合類型與交叉類型#
實際上,正如聯合類型的符號是|
,它代表了按位或,即只需要符合聯合類型中的一個類型,既可以認為實現了這個聯合類型,如A | B
,只需要實現 A 或 B 即可。
而代表著按位與的 &
則不同,你需要符合這裡的所有類型,才可以說實現了這個交叉類型,即 A & B
,需要同時滿足 A 與 B 兩個類型才行。
interface NameStruct {
name: string;
}
interface AgeStruct {
age: number;
}
type ProfileStruct = NameStruct & AgeStruct;
const profile: ProfileStruct = {
name: "linbudu",
age: 18
}
type StrAndNum = string & number; // never
type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string
索引類型#
索引簽名類型#
索引簽名類型主要指的是在介面或類型別名中,通過以下語法來快速聲明一個鍵值類型一致的類型結構:
interface AllStringTypes {
[key: string]: string;
}
type PropType1 = AllStringTypes['linbudu']; // string
type PropType2 = AllStringTypes['599']; // string
但由於 JavaScript 中,對於 obj[prop]
形式的訪問會將數字索引訪問轉換為字符串索引訪問,也就是說, obj[599]
和 obj['599']
的效果是一致的。因此,在字符串索引簽名類型中我們仍然可以聲明數字類型的鍵。類似的,symbol 類型也是如此:
const foo: AllStringTypes = {
"linbudu": "599",
599: "linbudu",
[Symbol("ddd")]: 'symbol',
}
索引簽名類型也可以和具體的鍵值對類型聲明並存,但這時這些具體的鍵值類型也需要符合索引簽名類型的聲明:
interface AllStringTypes {
// 類型“number”的屬性“propA”不能賦給“string”索引類型“boolean”。
propA: number;
[key: string]: boolean;
}
interface StringOrBooleanTypes {
propA: number;
propB: boolean;
[key: string]: number | boolean;
}
索引簽名類型的一個常見場景是在重構 JavaScript 代碼時,為內部屬性較多的對象聲明一個 any 的索引簽名類型,以此來暫時支持對類型未明確屬性的訪問,並在後續一點點補全類型:
interface AnyTypeHere {
[key: string]: any;
}
const foo: AnyTypeHere['linbudu'] = 'any value';
索引類型查詢#
keyof 操作符可以將對象中的所有鍵轉換為對應字面量類型,然後再組合成聯合類型。注意,這裡並不會將數字類型的鍵名轉換為字符串類型字面量,而是仍然保持為數字類型字面量。
interface Foo {
linbudu: 1,
599: 2
}
type FooKeys = keyof Foo; // "linbudu" | 599
// 在 VS Code 中懸浮鼠標只能看到 'keyof Foo'
// 看不到其中的實際值,你可以這麼做:
type FooKeys = keyof Foo & {}; // "linbudu" | 599
索引類型訪問#
索引類型查詢的本質其實就是,通過鍵的字面量類型訪問這個鍵對應的鍵值類型。
interface NumberRecord {
[key: string]: number;
}
type PropType = NumberRecord[string]; // number
interface Foo {
propA: number;
propB: boolean;
}
type PropAType = Foo['propA']; // number
type PropBType = Foo['propB']; // boolean
interface Foo {
propA: number;
propB: boolean;
propC: string;
}
type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean
映射類型#
映射類型的主要作用即是基於鍵名映射到鍵值類型
type Stringify<T> = {
[K in keyof T]: string;
};
interface Foo {
prop1: string;
prop2: number;
prop3: boolean;
prop4: () => void;
}
type StringifiedFoo = Stringify<Foo>;
// 等價於
interface StringifiedFoo {
prop1: string;
prop2: string;
prop3: string;
prop4: string;
}
這個工具類型會接受一個對象類型(假設我們只會這麼用),使用 keyof 獲得這個對象類型的鍵名組成字面量聯合類型,然後通過映射類型(即這裡的 in 關鍵字)將這個聯合類型的每一個成員映射出來,並將其鍵值類型設置為 string。
type Clone<T> = {
[K in keyof T]: T[K];
};
這裡的T[K]
其實就是上面說到的索引類型訪問,我們使用鍵的字面量類型訪問到了鍵值的類型,這裡就相當於克隆了一個介面。需要注意的是,這裡其實只有K in
屬於映射類型的語法,keyof T
屬於 keyof 操作符,[K in keyof T]
的[]
屬於索引簽名類型,T[K]
屬於索引類型訪問。
類型查詢操作符 Type Query Operator#
const str = "linbudu";
const obj = { name: "linbudu" };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
return input.length > 10;
}
type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean
const func = (input: string) => {
return input.length > 10;
}
const func2: typeof func = (name: string) => {
return name === 'linbudu'
}
const func = (input: string) => {
return input.length > 10;
}
// boolean
type FuncReturnType = ReturnType<typeof func>;
絕大部分情況下,typeof 返回的類型就是當你把鼠標懸浮在變量名上時出現的推導後的類型,並且是最窄的推導程度(即到字面量類型的級別)。你也不必擔心混用了這兩種 typeof,在邏輯代碼中使用的 typeof 一定會是 JavaScript 中的 typeof,而類型代碼(如類型標註、類型別名中等)中的一定是類型查詢的 typeof 。同時,為了更好地避免這種情況,也就是隔離類型層和邏輯層,類型查詢操作符後是不允許使用表達式的:
const isInputValid = (input: string) => {
return input.length > 10;
}
// 不允許表達式
let isValid: typeof isInputValid("linbudu");
類型守衛#
TypeScript 中提供了非常強大的類型推導能力,它會隨著你的代碼邏輯不斷嘗試收窄類型,這一能力稱之為類型的控制流分析(也可以簡單理解為類型推導)。這即是編程語言的類型能力中最重要的一部分:與實際邏輯緊密關聯的類型。我們從邏輯中進行類型地推導,再反過來讓類型為邏輯保駕護航。
function isString(input: unknown): boolean {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 類型“string | number”上不存在屬性“replace”。
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
因為 isString 這個函數在另外一個地方,內部的判斷邏輯並不在函數 foo 中。這裡的類型控制流分析做不到跨函數上下文來進行類型的信息收集(但別的類型語言中可能是支持的)。
實際上,將判斷邏輯封裝起來提取到函數外部進行復用非常常見。為了解決這一類型控制流分析的能力不足, TypeScript 引入了 is 關鍵字來顯式地提供類型信息:
function isString(input: unknown): input is string {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 正確了
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
is string
,即 is 關鍵字 + 預期類型,即如果這個函數成功返回為 true,那麼 is 關鍵字前這個入參的類型,就會被這個類型守衛調用方後續的類型控制流分析收集到。
需要注意的是,類型守衛函數中並不會對判斷邏輯和實際類型的關聯進行檢查:
function isString(input: unknown): input is number {
return typeof input === "string";
}
function foo(input: string | number) {
if (isString(input)) {
// 報錯,在這裡變成了 number 類型
(input).replace("linbudu", "linbudu599")
}
if (typeof input === 'number') { }
// ...
}
從這個角度來看,其實類型守衛有些類似於類型斷言,但類型守衛更寬容,也更信任你一些。你指定什麼類型,它就是什么類型。
export type Falsy = false | "" | 0 | null | undefined;
export const isFalsy = (val: unknown): val is Falsy => !val;
// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;
export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);
基於 in 與 instanceof 的類型保護#
interface Foo {
foo: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
bar: string;
barOnly: boolean;
shared: number;
}
function handle(input: Foo | Bar) {
if ('foo' in input) {
input.fooOnly;
} else {
input.barOnly;
}
}
function handle(input: Foo | Bar) {
if ('shared' in input) {
// 類型“Foo | Bar”上不存在屬性“fooOnly”。類型“Bar”上不存在屬性“fooOnly”。
input.fooOnly;
} else {
// 類型“never”上不存在屬性“barOnly”。
input.barOnly;
}
}
這個時候肯定有人想問,為什麼 shared 不能用來區分?答案很明顯,因為它同時存在兩個類型中不具有辨識度。而 foo /bar 和 fooOnly /barOnly 是各個類型獨有的屬性,因此可以作為可辨識屬性(Discriminant Property 或 Tagged Property)。Foo 與 Bar 又因為存在這樣具有區分能力的辨識屬性,可以稱為可辨識聯合類型(Discriminated Unions 或 Tagged Union)。雖然它們是一堆類型的聯合體,但其中每一個類型都具有一個獨一無二的,能讓它鶴立雞群的屬性。
這個可辨識屬性可以是結構層面的,比如結構 A 的屬性 prop 是數組,而結構 B 的屬性 prop 是對象,或者結構 A 中存在屬性 prop 而結構 B 中不存在。
它甚至可以是共同屬性的字面量類型差異:
function ensureArray(input: number | number[]): number[] {
if (Array.isArray(input)) {
return input;
} else {
return [input];
}
}
interface Foo {
kind: 'foo';
diffType: string;
fooOnly: boolean;
shared: number;
}
interface Bar {
kind: 'bar';
diffType: number;
barOnly: boolean;
shared: number;
}
function handle1(input: Foo | Bar) {
if (input.kind === 'foo') {
input.fooOnly;
} else {
input.barOnly;
}
}
function handle2(input: Foo | Bar) {
// 報錯,並沒有起到區分的作用,在兩個代碼塊中都是 Foo | Bar
if (typeof input.diffType === 'string') {
input.fooOnly;
} else {
input.barOnly;
}
}
class FooBase {}
class BarBase {}
class Foo extends FooBase {
fooOnly() {}
}
class Bar extends BarBase {
barOnly() {}
}
function handle(input: Foo | Bar) {
if (input instanceof FooBase) {
input.fooOnly();
} else {
input.barOnly();
}
}
類型斷言守衛#
斷言守衛和類型守衛最大的不同點在於,在判斷條件不通過時,斷言守衛需要拋出一個錯誤,類型守衛只需要剔除掉預期的類型。
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg);
}
}
這裡使用的是 asserts condition
,而 condition 來自於實際邏輯!這也意味著,我們將 condition 這一邏輯層面的代碼,作為了類型層面的判斷依據,相當於在返回值類型中使用一個邏輯表達式進行了類型標註。
let name: any = 'linbudu';
function assertIsNumber(val: any): asserts val is number {
if (typeof val !== 'number') {
throw new Error('Not a number!');
}
}
assertIsNumber(name);
// number 類型!
name.toFixed();
在這種情況下,你無需再為斷言守衛傳入一個表達式,而是可以將這個判斷用的表達式放進斷言守衛的內部,來獲得更獨立地代碼邏輯。
References:
https://juejin.cn/book/7086408430491172901