假設這樣一個場景,目前業務上僅對接了三方支付 'Alipay', 'Wxpay', 'PayPal'
, 實際業務 getPaymentMode
會根據不同支付方式進行不同的付款/結算流程。
const PAYMENT_MODE = ['Alipay', 'Wxpay', 'PayPal'];
function getPaymentMode(paymode: string) {
return PAYMENT_MODE.find(thirdPay => thirdPay === paymode)
}
getPaymentMode('Alipay') // ✔️
getPaymentMode('Wxpay') // ✔️
getPaymentMode('PayPal') // ✔️
getPaymentMode('unknow') // ✔️ 正常編譯,但可能引發執行時邏輯錯誤
由於宣告僅約束了入參 string
型別,無法避免由於手誤或上層業務處理傳參不當引起的執行時邏輯錯誤。
可以通過宣告字面量聯合型別來解決上述問題。
const PAYMENT_MODE = ['Alipay', 'Wxpay', 'PayPal'];
type mode = 'Alipay' | 'Wxpay' | 'PayPal';
function getPaymentMode(paymode: mode) {
return PAYMENT_MODE.find(thirdPay => thirdPay === paymode)
}
getPaymentMode('Alipay') // ✔️
getPaymentMode('Wxpay') // ✔️
getPaymentMode('PayPal') // ✔️
getPaymentMode('unknow') // ❌ Argument of type '"unknow"' is not assignable to parameter of type 'mode'.(2345)
字面量聯合型別雖然解決了問題,但是需要保持值陣列和聯合型別之間的同步,且存在冗餘。
兩者宣告在同一個檔案時,問題尚且不大。若 PAYMENT_MODE
由第三方庫提供,對方非 TypeScript
技術棧無法提供型別檔案,那要保持同步就比較困難,新增支付型別或支付渠道合作終止,都會引入潛在風險。
const PAYMENT_MODE = ['Alipay', 'Wxpay', 'PayPal'] as const; //亦可 import { PAYMENT_MODE } from 'outer'
type mode = typeof PAYMENT_MODE[number] // "Alipay" | "Wxpay" | "PayPal" 1)
function getPaymentMode(paymode: mode) {
return PAYMENT_MODE.find(thirdPay => thirdPay === paymode)
}
getPaymentMode('Alipay') // ✔️
getPaymentMode('Wxpay') // ✔️
getPaymentMode('PayPal') // ✔️
getPaymentMode('unknow') // ❌ Argument of type '"unknow"' is not assignable to parameter of type '"Alipay" | "Wxpay" | "PayPal"'.
1)處引入了本文的主角 typeof ArrayInstance[number]
完美的解決了上述問題,通過陣列值獲取對應型別。
typeof ArrayInstance[number] 如何拆解
首先可以確定 type mode = typeof PAYMENT_MODE[number]
在 TypeScript
型別宣告上下文 ,而非 JavaScript
變數宣告上下文。
PAYMENT_MODE
是陣列例項,number
是 TypeScript
數字型別。若是 PAYMENT_MODE[number]
組合,則語法不正確,陣列例項索引操作 []
中只能具體數字, 不能是型別。
所以 typeof PAYMENT_MODE[number]
等同於 (typeof PAYMENT_MODE)[number]
。
可以看出 typeof PAYMENT_MODE
是一個陣列型別
type mode1 = typeof PAYMENT_MODE // readonly ["Alipay", "Wxpay", "PayPal"]
typeof PAYMENT_MODE[number] 等效 mode1[number]
,我們知道 mode1[]
是 indexed access types
,[]
中 Index
來源於 Index Type Query
也即 keyof
操作 。
type mode1 =keyof typeof PAYMENT_MODE
// number | "0" | "1" | "2" | "length" | "toString" | "toLocaleString" | "concat" | "join" | "slice" | "indexOf" | "lastIndexOf" | "every" | "some" | "forEach" | "map" | "filter" | ... 7 more ... | "includes"
可以看出得到的聯合型別第一項就是 number
型別,我們常見 keyof
得到的都是型別屬性名組成的字串字面量聯合型別,如下所示,那這個 number
是怎麼來的。
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
從 TypeScript-2.9 文件可以看出,
如果 X 是物件型別, keyof X 解析規則如下:
- 如果 X 包含字串索引簽名, keyof X 則是由string 、number 型別, 以及symbol-like 屬性字面量型別組成的聯合型別, 否則
- 如果 X 包含數字索引簽名, keyof X 則是由number型別 , 以及string-like 、symbol-like 屬性字面量型別組成的聯合型別, 否則
- keyof X 由 string-like, number-like, and symbol-like 屬性字面量型別組成的聯合型別.
其中
- 物件型別的 string-like 屬性可以是 an identifier, a string literal, 或者 string literal type的計算屬性名 .
- 物件型別的number-like 屬性可以是 a numeric literal 或 numeric literal type 的計算屬性名.
- 物件型別的symbol-like 屬性可以是a unique symbol type的計算屬性名.
示例如下:
const c = "c1";
const d = 10;
const e = Symbol();
const enum E1 {
A
}
const enum E2 {
A = "A"
}
type Foo1 = {
"f": string, // String-like 中 a string literal
["g"]:string; // String-like 中 計算屬性名
a: string; // String-like 中 identifier
[c]: string; // String-like 中 計算屬性名
[E2.A]: string; // String-like 中計算屬性名
5: string; // Number-like 中 numeric literal
[d]: string; // Number-like 中 計算屬性名
[E1.A]: string; // Number-like 中 計算屬性名
[e]: string; // Symbol-like 中 計算屬性名
};
type K11 = keyof Foo1; // type K11 = "c1" | E2.A | 10 | E1.A | typeof e | "f" | "g" | "a" | 5
再次回到前面內容:
type payType = typeof PAYMENT_MODE; // readonly ["Alipay", "Wxpay", "PayPal"]
type mode1 =keyof typeof PAYMENT_MODE
// number | "0" | "1" | "2" | "length" | "toString" | "toLocaleString" | "concat" | "join" | "slice" | "indexOf" | "lastIndexOf" | "every" | "some" | "forEach" | "map" | "filter" | ... 7 more ... | "includes"
編譯器提示的 readonly ["Alipay", "Wxpay", "PayPal"
型別不夠具象,我們無從得知 payType
具體有哪些屬性。
keyof typeof PAYMENT_MODE
只有 number
型別而沒有 string
型別,根據上面 keyof
解析規則的第2條,可以推斷 typeof PAYMENT_MODE
型別含有數字索引簽名,以及之前的結果 type mode = typeof PAYMENT_MODE[number] // "Alipay" | "Wxpay" | "PayPal"
。
我們可以據此推測出 payType
更加直觀的型別結構:
type payType = {
[i :number]: "Alipay" | "Wxpay" | "PayPal"; //數字索引簽名
"length": number;
"0": "Alipay"; //因為陣列可以通過數字或字串訪問
"1": "Wxpay";
....
"toString": string;
//省略其餘陣列方法屬性
.....
}
type eleType = payType[number] // "Alipay" | "Wxpay" | "PayPal"
後來我在 lib.es5.d.ts 中找到了 ReadonlyArray
interface ReadonlyArray<T> {
readonly length: number;
toString(): string;
//......省略中間函式
readonly [n: number]: T;
}
值得一提的是,ReadonlyArray
型別結構中,沒有常規陣列 push
等寫操作方法名的 key
。
const immutable = ['a', 'b', 'c'] as const;
immutable[2]; //✔️
immutable[4]; //❌ // length '3' has no element at index '4'
immutable.push ;//❌ //Property 'push' does not exist on type 'readonly ["a", "b", "c"]'
immutable[0] = 'd'; // ❌ Cannot assign to '0' because it is a read-only property
const mutable = ['a', 'b', 'c'] ;
mutable[2]; //✔️
mutable[4]; //✔️
mutable.push('d'); //✔️
由於陣列是物件,所以 mutable 是引用,即使用const宣告變數, 依然可以修改陣列中元素。得益於as const的型別斷言,編譯期可以確定ReadonlyArray 型別,無法修改陣列,編譯器就可以動態生成如下型別。
type indexLiteralType = {
"0": "Alipay" ;
"1": "Wxpay";
"2": "PayPal";
}
按照設計模式中介面單一職責原則, 可以推斷 payType (readonly ["Alipay", "Wxpay", "PayPal"])
是由ReadonlyArray 只讀型別和 indexLiteralType 字面量型別組成的聯合型別。
type indexLiteralType = {
readonly "0": "Alipay" ,
readonly "1": "Wxpay",
readonly "2": "PayPal"
};
type values = indexLiteralType [keyof indexLiteralType ];
type payType = ReadonlyArray<values> & indexLiteralType;
type test1 = payType extends (typeof PAYMENT_MODE) ? true:false; //false
type test2 = (typeof PAYMENT_MODE) extends payType ? true:false; //true
type test3 = payType[number] extends (typeof PAYMENT_MODE[number]) ? true:false; //true
type test4 = (typeof PAYMENT_MODE[number]) extends payType[number] ? true:false; //true
這裡我們構造出的 payType
是 typeof PAYMENT_MODE
的父型別,已經非常接近了,還需要再和其他型別進行聯合才能得到一樣的型別,現在 payType
的具象程度已經足夠我們理解typeof PAYMENT_MODE
了,不再進一步構造一樣的型別,因目前掌握的資訊可能無法構造完全一樣的型別。
藉助 typeof ArrayInstance[number]
從常量值陣列中獲取對應元素字面量型別 的剖析至此結束 。