高级 TypeScript:映射类型

更新日期: 2022-07-09阅读: 703标签: 类型

使用强类型语言会带来很多好处,TypeScript也不例外:你使用的类型越强,就能获得越好的结果。不幸的是,TypeScript 的灵活性让我们能够使用一种大得多的类型去描述某些对象,而这些对象原本可以使用更窄更有效的类型去建模。其中一个场景就是使用字符串和数字建模。

基本类型,例如 string 或 number,对于处理极大数据的数值是有意义的。但是,很多情形下,我们关心的只是有限个字符串(或其它基本类型)。我们当然可以在运行时去检测这种值是不是合法,但 TypeScript 也提供了一些机制,让我们能够更好地对这样的值建模。

本文,我们将以一个非常常见的需求为例,来展示 TypeScript 的某些不大常见的特性的应用。我们的例子是多地区多语言的站点。我们将展示 TypeScript 的如下特性:

  • as const 表达式
  • keyof 和 typeof
  • 泛型中的动态类型参数推断
  • 映射类型
  • 在联合中使用 never 过滤掉某些类型

一个场景

假设我们正在开发一个适用于多个国家和地区的网站。每个国家都有自己版本的站点,并且提供多种不同的语言。同时,我们想要根据下面的配置,在某些地区禁用某些语言。

const EnabledRegons = {
  "GB": {
    "en": true,
  },
  "IT": {
    "en": true,
    "it": true,
  },
  "PL": {
    "pl": true,
    "en": false,
  },
  "LU": {
    "fr": true,
    "de": true,
    "en": true,
  }
} as const;

因为我们知道,这个配置是不会被修改的,因此,我们可以利用 as const 表达式,将其定义为只读的。

我们可以利用 TypeScript 的 playgroud 页面 ,看看这样的代码的 .d.ts 文件究竟是什么样子。

当没有 as const 表达式时,

TypeScript

.d.ts

const EnabledRegons = {
  "GB": {
    "en": true,
  },
  "IT": {
    "en": true,
    "it": true,
  },
  "PL": {
    "pl": true,
    "en": false,
  },
  "LU": {
    "fr": true,
    "de": true,
    "en": true,
  }
};
declare const EnabledRegons: {
    GB: {
        en: boolean;
    };
    IT: {
        en: boolean;
        it: boolean;
    };
    PL: {
        pl: boolean;
        en: boolean;
    };
    LU: {
        fr: boolean;
        de: boolean;
        en: boolean;
    };
};

如果添加了 as const 表达式,

TypeScript

.d.ts

const EnabledRegons = {
  "GB": {
    "en": true,
  },
  "IT": {
    "en": true,
    "it": true,
  },
  "PL": {
    "pl": true,
    "en": false,
  },
  "LU": {
    "fr": true,
    "de": true,
    "en": true,
  }
} as const;
declare const EnabledRegons: {
    readonly GB: {
        readonly en: true;
    };
    readonly IT: {
        readonly en: true;
        readonly it: true;
    };
    readonly PL: {
        readonly pl: true;
        readonly en: false;
    };
    readonly LU: {
        readonly fr: true;
        readonly de: true;
        readonly en: true;
    };
};

可以看到,当我们使用了 as const 表达式时,TypeScript 知道这些值不可能被修改,因此,在生成 .d.ts 文件时,TypeScript 将对象属性进行了冻结,防止类型扩大。而没有使用 as const 的 .d.ts 文件,属性值仅仅被定义为 boolean ,这就意味着可能被重新赋值。

获取国家名字

下面,我们需要一个函数实现根据国家代码返回国家名字。这个简单:

const countryCodeToName = (countryCode: string): string => {
  switch (countryCode) {
    case "GB": return "Great Britain";
    case "IT": return "Italy";
    case "PL": return "Poland";
    case "LU": return "Luxembourg";
  }
}

虽然上面代码中的 switch 其实已经覆盖了所有情形,但 TypeScript 还是会报错,因为我们没有给 switch 添加 default 分支。为了满足 TypeScript 的要求,我们必须添加一个 default 分支,即便我们知道这个分支永远不会走到。但实际情况并不是仅仅一个 default 分支那么简单:

  • 我们引入了一段永远不可能执行到的代码
  • 如果我们决定要移除一个地区,那么就会在应用程序中得到一段再也不会执行到的代码
  • 如果我们要添加一个地区,那么就得找找我们要在哪添加——TypeScript 可不会告诉我们要在哪加代码

这些问题的引入来自于这个函数的参数类型是 string 这么一个通用类型,而这个类型远远大于实际值的可选范围——实际值只有 GB 、 IT 、 PL 和 LU 这么四个。所以,这个函数的参数类型应该是 EnabledRegons 这个类型的所有键的集合。那么,我们可以将函数修改为:

const countryCodeToName = (countryCode: keyof typeof EnabledRegons): string => {
  switch (countryCode) {
    case "GB": return "Great Britain";
    case "IT": return "Italy";
    case "PL": return "Poland";
    case "LU": return "Luxembourg";
  }
}

现在,TypeScript 再也不会抱怨缺少 default 分支了,因为我们已经覆盖了所有路径。另外,如果你要删除地区代码,TypeScript 就会报错,因为你使用了不是 EnabledResons 的键的值,从而可以很容易找到需要删除的代码。如果要添加新的地区,TypeScript 同样会报错,因为我们没有覆盖所有情况。

要理解 keyof typeof 的使用,我们首先要理解字面量类型 literal types 以及字面量类型的联合 union of literal types 。

字面量类型

我们可以把字面量类型理解成一种更特殊的 string 、 number 或者 boolean 。比如, "Hello, world!" 是 string ,但 string 不是 "Hello, world!" 。 "Hello, world!" 是一种更特殊的 string ,因此它是字面量类型。

我们可以这样定义字面量类型:

type Greeting = "Hello";

当我们将一个变量定义为字面量类型时,意味着这个变量只能接受这个字面量。例如:

let greeting: Greeting;
greeting = "Hello"; // OK
greeting = "Hi";    // Error: Type '"Hi"' is not assignable to type '"Hello"'

这里,因为 Greeting 是一个字面量类型,所以,变量 greeting 只能赋值为这个字面量的值,其它任何值都是不允许的。

这很像是常量,但常量可以初始化为任意值,常量不是一种类型,只是一个值,而字面量仅允许单一值,是一种类型。

单一的字面量类型作用并不大,更大的作用是将若干字面量联合起来,也就是字面量的联合:

type Greeting = "Hello" | "Hi" | "Welcome";

现在, Greeting 类型更强大了:

let greeting: Greeting;
greeting = "Hello";       // OK
greeting = "Hi";          // OK
greeting = "Welcome";     // OK
greeting = "GoodEvening"; // Error: Type '"GoodEvening"' is not assignable to type 'Greeting'

利用这种技术,我们其实是创建了一个仅包含有限个元素的集合。利用这个集合,我们将变量的可选值限制在一定的范围内。

keyof

那么, keyof 运算符是什么意思呢? keyof T 的含义是,返回一个新的字面量类型的联合类型,其中,字面量类型来自于这个 T 类型中所有的属性名。例如:

interface Person {
  name: string;
  age: number;
  location: string;
}

对 Person 类型使用 keyof 运算符:

type SomeType = keyof Person; // "name" | "age" | "location"

然后,我们就可以使用这个类型了:

let obj: SomeType;
obj = "name";      // OK
obj = "age";       // OK
obj = "location";  // OK
obj = "something"; // Error...

keyof typeof

typeof 运算符是 JavaScript 的运算符,作用是返回一个变量的类型。

上面的例子中,我们已经知道 Persion 类型,因此可以直接对其使用 keyof 运算符。但如果我们只有一个变量,并不知道具体的类型,就不能直接使用 keyof 了。此时,我们就需要先使用 typeof 运算符,获得这个变量的类型,然后再使用 keyof 运算符。

例如,

const persion = {
  name: "Tome",
  age: 12
};

type NewType = keyof typeof persion;
let newType: NewType;
newType = "name";     // OK
newType = "age";      // OK
newType = "newValue"; // Error...

上面我们看到 keyof typeof 作用于一个对象。那么,如果是枚举呢?

在 TypeScript 中,枚举是编译期的类型,等同于编译期类型安全的常量;但在运行时,枚举退化为一个对象。这一点我们可以由 TypeScript 的编译结果看出。例如,

enum Colors {
    white = '#ffffff',
    black = '#000000',
}

经过编译之后为:

"use strict";
var Colors;
(function (Colors) {
    Colors["white"] = "#ffffff";
    Colors["black"] = "#000000";
})(Colors || (Colors = {}));

因此,针对枚举使用 keyof typeof 运算符,与针对对象并没有本质的区别。

type ColorTypes = keyof typeof Colors

let colorLiteral: ColorTypes;
colorLiteral = "white"  // OK
colorLiteral = "black"  // OK
colorLiteral = "red"    // Error...

如果你想知道一段 TypeScript 代码被翻译成怎样的 JavaScript 代码,可以打开 TypeScript 的官方网站的页面: https://www.typescriptlang.org/play 。这里以左右对照的形式展示了 TypeScript 的翻译结果。

根据地区对语言进行建模

假如我们有一个函数 getUrl() ,可以根据地区代码和语言代码返回一个适用于这个地区的这个语言的 URL。为严格起见,我们必须按照前面的那个配置信息调用这个函数,当传入了不匹配的地区和语言时,函数就会报错:

etUrl("GB", "en"); // 正确
getUrl("IT", "it"); // 正确
getUrl("IT", "pl"); // 错误,因为 IT 没有 pl 语言

幸运的是,我们可以使用类型参数推断 type argument inference 对这个函数进行改造:

const getUrl = <CountryCode extends keyof typeof EnabledRegons>
  (country: CountryCode,
  language: keyof typeof EnabledRegons[CountryCode]): string => {
    // body of our function
}

这种技术对于一个参数依赖于另外参数的情形尤其重要。其中一个很常见的应用场景是 addEventListener 函数:该函数根据事件类型,确定其回调函数的参数类型。实际代码可以在 这里 找到。我们将其摘录出来:

addEventListener<K extends keyof htmlElementEventMap>(type: K, listener: (this: HTMLAnchorElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;

注意,这里的 HTMLElementEventMap 是以字符串为键、 Event 对象类型为值的对照关系。

动态创建 Locale 字符串

如果你曾注意到多语言网站,往往有一种同时指定地区和语言的字符串:Locale。这种字符串包含有地区代码和语言代码,并且以连字符相连。我们重构一下前面的 getUrl() 函数,使用 locale 字符串作为参数类型。这意味着,我们必须能够动态创建 locale 字符串。幸运的是,我们有映射类型:

type ValueOf<T> = T[keyof T];

type Locale = ValueOf<{[K in Region]: `${keyof typeof EnabledRegons[K] & string}-${K}`}>

Locale 类型通过遍历地区代码进行创建,同时,对于每一个地区,生成一个包含了语言代码的地区代码。这里有一个小小的技巧 & string 。这是因为 keyof 运算符返回类型是 string | number | symbol ,我们需要告诉 TypeScript,我们仅关心 string 类型。

这样,TypeScript 还可以帮我们实现代码提示:


过滤

下面我们可以继续改进代码。我们可以根据配置信息,自动排除禁用掉的语言。

type ExcludeFalseValues<T> = {[K in keyof T as T[K] extends true ? K : never]: T[K] }

type Locale = ValueOf<{
    [K in Region]: `${keyof ExcludeFalseValues<typeof EnabledRegons[K]> & string}-${K}`
}>

这段代码使用 never 过滤掉不需要的值。当一个值不是扩展自 true 时,也就是 TypeScript 或去检查是否相同,我们通过设置 never 告诉 TypeScript 忽略掉这个属性。

现在,我们的代码已经足够智能,当我们禁用掉某一地区的某一语言时,TypeScript 就可以检测出来:


这样技术对于找出与停用的代码相关的其它代码,或者在维护翻译时,确保不同语言之间的值是同步的时,尤其有用。

结论

TypeScript 的映射类型以及其它相关技术,对于创建更严格的类型代码非常有帮助。另一方面,虽然看起来繁琐,但这种代码通常会带来很多意想不到的好处。

原文 https://www.devbean.net/2022/07/ts-mapped-types/

链接: https://www.fly63.com/article/detial/11877

JS中Null与Undefined的区别

在JavaScript中存在这样两种原始类型:Null与Undefined。这两种类型常常会使JavaScript的开发人员产生疑惑,在什么时候是Null,什么时候又是Undefined?Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。

Javascript的类型检测

主要介绍了JS中检测数据类型的几种方式,typeof运算符用于判断对象的类型,但是对于一些创建的对象,它们都会返回\'object\',有时我们需要判断该实例是否为某个对象的实例,那么这个时候需要用到instanceof运算符

js类型转换的各种玩法

对于object和number、string、boolean之间的转换关系,ToPrimitive是指转换为js内部的原始值,如果是非原始值则转为原始值,调用valueOf()和toString()来实现。

JavaScript类型:关于类型,有哪些你不知道的细节?

Undefined类型表示未定义,它的类型只有一个值为undefined。undefined和null有一定的表意差别。非整数的Number类型无法使用 == 或 === 来比较,因为 JS 是弱类型语言,所以类型转换发生非常频繁

为你的 JavaScript 项目添加智能提示和类型检查

近在做项目代码重构,其中有一个要求是为代码添加智能提示和类型检查。智能提示,英文为 IntelliSense,能为开发者提供代码智能补全、悬浮提示、跳转定义等功能,帮助其正确并且快速完成编码。

js的类型

基本类型:按值访问,可以操作保存在变量中实际的值;引用类型数据存在堆内存,而引用存在栈区,也就是说引用类型同时保存在栈区和堆区,关于==的执行机制,ECMASript有规范,因为==前后的值交换顺序,返回的值也是一样的,所以在此对规范做出如下总结

再也不用担心 JavaScript 的数据类型转换了

JavaScript 是一种弱类型或者说动态类型语言。所以你不用提前声明变量的类型,在程序运行时,类型会被自动确定,你也可以使用同一个变量保存不同类型的数据。

JavaScript基础之值传递和引用传递

js的值传递和引用(地址)传递:js的5种基本数据类型 number,string,null,undefined,boolean 在赋值传递时是值传递,js的引用数据类型(object,array,function)进行引用传递,其实底层都是对象。

JS中的布尔 数字 字符串

JS中所有的值都可以转换成布尔类型 使用Boolean()或者 !!(两个感叹号),JS中所有的值都可以转换成数字类型,使用Number()或+。数字类型转换场景目的只有一个,用于计算,将后台传递的数据,从字符串转换为数字并参与计算

if条件中,js的强制类型转换

众所周知,JS在很多情况下会进行强制类型转换,其中,最常见两种是:1.使用非严格相等进行比较,对==左边的值进行类型转换2.在if判断时,括号内的值进行类型转换,转化为布尔值

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!