TypeScript中 typeof ArrayInstance[number] 剖析


假設這樣一個場景,目前業務上僅對接了三方支付 '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 是數組實例,numberTypeScript數字類型。若是 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 解析規則如下:

  1. 如果 X 包含字符串索引簽名, keyof X 則是由string 、number 類型, 以及symbol-like 屬性字面量類型組成的聯合類型, 否則
  2. 如果 X 包含數字索引簽名, keyof X 則是由number類型 , 以及string-like 、symbol-like 屬性字面量類型組成的聯合類型, 否則
  3. keyof X 由 string-like, number-like, and symbol-like 屬性字面量類型組成的聯合類型.

其中

  1. 對象類型的 string-like 屬性可以是 an identifier, a string literal, 或者 string literal type的計算屬性名 .
  2. 對象類型的number-like 屬性可以是 a numeric literal 或 numeric literal type 的計算屬性名.
  3. 對象類型的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] 從常量值數組中獲取對應元素字面量類型 的剖析至此結束 。

示例地址 Playground


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM