乐趣区

关于前端:TypeScript中-typeof-ArrayInstancenumber-剖析

假如这样一个场景,目前业务上仅对接了三方领取 '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
  : 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<T> 类型,更进一步验证了下面的揣测:

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

退出移动版