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

TypeScript中 typeof ArrayInstance[number] 剖析的更多相关文章

  1. JavaScript中typeof、toString、instanceof、constructor与in

    JavaScript 是一种弱类型或者说动态语言.这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定. 这也意味着你可以使用同一个变量保存不同类型的数据. 最新的 ECMAScrip ...

  2. 【JavaScript中typeof、toString、instanceof、constructor与in】

    JavaScript中typeof.toString.instanceof.constructor与in JavaScript 是一种弱类型或者说动态语言.这意味着你不用提前声明变量的类型,在程序运行 ...

  3. TypeScript 中的方法重载

    方法重载(overload)在传统的静态类型语言中是很常见的.JavaScript 作为动态语言, 是没有重载这一说的.一是它的参数没有类型的区分,二是对参数个数也没有检查.虽然语言层面无法自动进行重 ...

  4. typeScript中的函数

    // 函数的定义 //es5定义函数的方法 /* //函数声明法 function run(){ return 'run'; } //匿名函数 var run2=function(){ return ...

  5. 聊聊 TypeScript 中的类型保护

    聊聊 TypeScript 中的类型保护 在 TypeScript 中使用联合类型时,往往会碰到这种尴尬的情况: interface Bird { // 独有方法 fly(); // 共有方法 lay ...

  6. TypeScript 中限制对象键名的取值范围

    当我们使用 TypeScript 时,我们想利用它提供的类型系统限制代码的方方面面,对象的键值,也不例外. 譬如我们有个对象存储每个年级的人名,类型大概长这样: type Students = Rec ...

  7. typescript 中的 infer 关键字的理解

    infer 这个关键字,整理记录一下,避免后面忘记了.有点难以理解呢. infer infer 是在 typescript 2.8中新增的关键字. infer 可以在 extends 条件类型的字句中 ...

  8. TypeScript 中函数的理解?与 JavaScript 函数的区别?

    一.是什么 函数是JavaScript 应用程序的基础,帮助我们实现抽象层.模拟类.信息隐藏和模块 在TypeScript 里,虽然已经支持类.命名空间和模块,但函数仍然是主要定义行为的方式,Type ...

  9. 5种在TypeScript中使用的类型保护

    摘要:在本文中,回顾了TypeScript中几个最有用的类型保护,并通过几个例子来了解它们的实际应用. 本文分享自华为云社区<如何在TypeScript中使用类型保护>,作者:Ocean2 ...

随机推荐

  1. Kafka知识总结及面试题

    目录 概念 Kafka基础概念 命令行 Kafka 数据存储设计 kafka在zookeeper中存储结构 生产者 生产者设计 消费者 消费者设计 面试题 kafka设计 请说明什么是Apache K ...

  2. Servlet[JAX-RS Servlet]的Servlet.init()引发异常

    代码环境 Eclipse2017 : 问题出现: 在测试Hello servlet时发生 org.apache.catalina.core.ApplicationContext log严重: Serv ...

  3. RPC框架学习+小Demo实例

    一.什么是RPC协议? 全称:远程过程调度协议 效果:使消费者向调用本地方法一样调用远程服务方法,对使用者透明 目前常用:Dubbo.Thirft.Sofa.... 功能: 建立远程通信(socket ...

  4. OOP、封装、继承、多态,真的懂了吗?

    平时只要一提起来面向对象编程OOP的好处,随口就能说出来,不就是封装.继承.多态么,可他们的含义是什么呢,怎么体现,又有什么非用不可的好处啊.可能平时工作中天天在用OOP,仅仅是在用OOP语言,就是一 ...

  5. LeetCode876 链表的中间结点

    给定一个带有头结点 head 的非空单链表,返回链表的中间结点. 如果有两个中间结点,则返回第二个中间结点. 示例 1: 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4 ...

  6. 【Linux】snmp在message中报错: /etc/snmp/snmpd.conf: line 311: Error: ERROR: This output format has been de

    Apr 17 17:36:17 localhost snmpd[2810]: /etc/snmp/snmpd.conf: line 311: Error: ERROR: This output for ...

  7. webpack知识点整理

    作用域 es6里模块化的写法 会存在的问题,变量.方法名字雷同,外部文件调用的时候出现问题 如 a.js里 var a='susan' b.js里 var a='jack' 问题解决方案,添加包裹 a ...

  8. 十九、更改LCD显示屏

    一.裸机修改 之前测试用的屏幕是480*272的分辨率,现在要换成800*480的屏,因此要对软件代码进行修改. 对于裸机驱动而言,主要有两个点需要注意,一个是屏幕分辨率变了,因此初始化的时候与屏幕分 ...

  9. 浅谈java中线程和操作系统线程

    在聊线程之前,我们先了解一下操作系统线程的发展历程,在最初的时候,操作系统没有进程线程一说,执行程序都是串行方式执行,就像一个队列一样,先执行完排在前面的,再去执行后面的程序,这样的话很多程序的响应就 ...

  10. Soul 网关 Nacos 数据同步源码解析

    学习目标: 学习Soul 网关 Nacos 数据同步源码解析 学习内容: 环境配置 Soul 网关 Nacos 数据同步基本概念 源码分析 学习时间:2020年1月28号 早7点 学习产出: 环境配置 ...