当前位置 博文首页 > 冒泡机不冒泡了:TypeScript中 typeof ArrayInstance[number] 剖

    冒泡机不冒泡了:TypeScript中 typeof ArrayInstance[number] 剖

    作者:冒泡机不冒泡了 时间:2021-02-04 20:24

    假设这样一个场景,目前业务上仅对接了三方支付 '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

    bk