🚀🚀🚀Typescript通关秘籍(六)🔥🔥🔥

keyof

keyofTS 新增的一个运算符,它接受一个对象类型作为参数,会返回该对象类型的所有键名组成的联合类型。

type Person = {
  name: string,
  age: number,
}
type P = keyof Person; // 'name' | 'age';

let p1: P = 'name';
let p2: P = 'age';
let p3: P = 'sex'; // ❌

三种键名

JS 中,对象的键名只有三种类型:string、number、symbol

所以, keyof 运算符一般返回的类型就是 string | number | symbol 的联合类型。

const symbolKey = Symbol('橙某人');
interface Person {
  name: string,
  99: number,
  [symbolKey]: symbol,
}

type P = keyof Person; // 'name' | 99 | symbolKey

let p1: P = 'name';
let p2: P = 99;
let p3: P = symbolKey;

注意,使用 Symbol() 定义的值,作为类型别名、接口的键名,该值的声明只能用 const 声明,不能用 let

注意❗千万不要把它和 typeof 搞混了。

🍊关于 symbol 与 unique symbol 的区别?

可以看看大佬的说明:传送门

可以看看官网说明:传送门

应用

keyof 最常见的一个应用就是取出对象的某个指定属性的值。

function fn<T, K extends keyof T>(target: T, key: K): T[K] {
  return target[key];
}

以上函数能精准的限制 key 的传参,也能获取对应属性值的类型。

keyof any 返回的类型:string | number | symbol

in

in 运算符,在 TS 中的作用与 JS 有些区别,它能用来遍历出联合类型的每个成员类型。

type Keys = 'name' | 'age' | 'sex';

type Person = {
  [key in Keys]: string;
}

// 等价于
type Person = {
  name: string;
  age: string;
  sex: string;
}

infer

infer 这是 TS 新增的一个比较复杂的运算符。

它经常和泛型约束(extends)一起使用,不懂 extends 运算符的建议先看看前面泛型那里。

咱们先不探究它的概念,来看个例子。

1️⃣ 要求声明一个类型,它能获取数组/元组类型的第一项类型:

type FirstArrayItemType<T> = T extends [infer FirstItemType, ...any[]] ? FirstItemType : unknown;

type Arr1 = [number, string, boolean];
type Arr2 = [string, number, boolean];

type F1 = FirstArrayItemType<Arr1>; // number
type F2 = FirstArrayItemType<Arr2>; // string
type F3 = FirstArrayItemType<[]>; // unknown

👻咋样,能整懂不?😮

是不是有点像占位符的意思?它将第一项的类型命名成 FirstItemType 类型,再进行 extends 的判断,满足条件就返回该类型,不满足就返回 unknown 类型。

小编相信你能有些许体会了,再来看个例子。👀

2️⃣ 要求声明一个类型,如果传入的类型参数是数组类型,就返回数组元素的联合类型,否则传入什么类型参数就返回什么类型:

type isArray<T> = T extends Array<any> ? T[number] : T; // Array<any> 等同 [any]

type A1 = isArray<string>; // string
type A2 = isArray<number>; // number

type A3 = isArray<Array<string>>; // string  数组元素都是string类型
type A4 = isArray<number[]>; // number

type T4 = isArray<[string, number]>; // string | number
type T5 = isArray<[string, number, boolean]>; // string | number | boolean

❓❓❓ T[number] 是啥?(小小的脑袋,大大的疑问)

T[number] 牵扯的前因后果还比较复杂😲,它的结论是利用了数组的索引签名形式。

TS 中,我们如何去描述数组每一项的类型呢?答案是元组。

一般如下定义:

let arr: [string, number, boolean] = ['橙某人', 18, true];

// or

let arr = ['橙某人', 18, true];

用类型别名和接口声明的形式呢?(数组也是对象)如下:

interface ArrType1 {
  0: '橙某人';
  1: 18;
  2: true;
  length: 3;
}

// or

type ArrType2 = {
  0: '橙某人',
  1: 18,
  2: true,
  length: 3,
}

let arr1: ArrType1 = ['橙某人', 18, true];
let arr2: ArrType2 = ['橙某人', 18, true];

你发现没有?数组项的索引都是以数字来描述的。

所以,在 TS 中,用 T[number] 来获取匹配数组元素的联合类型,这是一个结论。

type ArrType1 = Array<string>;
type ArrType2 = [string, number, boolean];

type A1 = ArrType1[number]; // number
type A2 = ArrType2[number]; // string | number | boolean

当然,可能有小伙伴注意到,不是还有一个 length 属性吗?这是一个字符串索引,那会不会还有 T[string] 形式呢?

那当然是没有的。😐

type ArrType3 = [string, number, boolean];
type A3 = ArrType3[string]; // ❌ Type 'ArrType3' has no matching index signature for type 'string'

至于为什么会有这种奇异的情况呢?可以看看这个解释。传送门

等等❗❗❗ 小编是不是讲跑题了?我们不是在说 infer 运算符吗?呃……不要在意这些细节。😄

其实花点时间讲 T[number] 的意思,是为了做一下铺垫,下面我们直接把它干掉,换成 infer 看看效果。

type isArray<T> = T extends Array<infer U> ? U : T; // Array<infer U> 等同 [infer U]

type A1 = isArray<string>; // string
type A2 = isArray<number>; // number

type A3 = isArray<Array<string>>; // string
type A4 = isArray<number[]>; // number

type T4 = isArray<[string, number]>; // string | number
type T5 = isArray<[string, number, boolean]>; // string | number | boolean

可以发现结果是一样的。

从这两个数组的例子上看,infer 它既可以代表一个数组元素的类型,也可以代表一个数组所有元素的联合类型,这取决于数组本身的特性。

当然,不仅仅数组,字符串、对象、函数等都可以很灵活的应用 infer 运算符。

字符串

type getName<T> = T extends `我的名字叫${infer U}` ? U : '不知名';

type Name1 = getName<'我的名字叫橙某人'>; // 橙某人
type Name2 = getName<'掘金'>; // 不知名

对象

type getName<T> = T extends { name: infer U; } ? U : unknown;

type Name1 = getName<{ name: string }>; // string
type Name2 = getName<{ age: number }>; // unknown

函数

type getFnReturnValueType<T> = T extends () => infer U ? U : unknown;

type ReturnValueType1 = getFnReturnValueType<() => void>; // void
type ReturnValueType2 = getFnReturnValueType<() => string>; // string
type ReturnValueType3 = getFnReturnValueType<string>; // unknown

从上述例子中,相信你大概能体会到 infer 运算符的作用了。

最后,它的概念:inter 可以用来声明泛型里面推断出来的类型参数。

三横线指令

三横线指令(/// <reference path ="..."/>) 是 TS 早期的模块化方式,用于从其他文件中导入”类型”。

但是,在 ESM 模式广泛使用后,便不再推荐使用了,这里咱们来顺嘴提提。

三斜线指令最重要的一个注意点:它只能放在文件最顶端。

我们新建 Person.ts 文件:

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

再创建 Student.ts 文件:

/// <reference path="./Person.ts" />

let student: Person = {
  name: '橙某人',
  age: 18
}

三斜线指令,咱只要认识它的三个指令即可。

  • /// <reference path ="..."/>:用于引入我们自己编写的 .ts 文件或者 .d.ts 的声明文件。

  • /// <reference types="..." />:用于引入第三方声明文件。

/// <reference types="@types/node" />

// 它表示会引入 `node_modules/@types/node/index.d.ts` 的声明文件。
  • /// <reference lib="..." />:用于引入 TS 内置的声明文件。
/// <reference lib="es2020" />
/// <reference lib="esnext.asynciterable" />
/// <reference lib="esnext.intl" />
/// <reference lib="esnext.bigint" />

TS 内置的声明文件一般我们不手动引入,普遍都是通过 tsconfig.json 文件进行配置。

{
  "compilerOptions": {
    "lib": [
      "es2020", "es2019.array"
    ]
  },
}

命名空间 – namespace

命名空间namespace)是 TS 为了模块化格式发明的一个新玩意,它源自 JS 中的闭包概念,它的作用一样是为了避免全局作用域被”污染”。

不过,命名空间在 ESM 模块广泛使用后,官方已经不推荐使用了。

但是呢,我们应该也要有所了解才行哦。😲

像有时,咱们可能在 A.ts 文件中声明:

const name = '橙某人';
function fn() {};

然后继续在 B.ts 文件中声明:

const name = '橙某人'; // ❌
function fn() {}; // ❌

本来按 ES6 的单文件模块形式,这样子是被允许的。

可惜,这却在 TS 中行不通了,如果我们期望某个成员声明仅仅作用于局部的作用域,这会就需要我们使用命名空间了。

这是因为在 TS 中,如果一个文件中不包含 export 语句,那它就是一个全局的脚本文件。相应地,任何包含 importexport 语句的文件,就是一个模块(module)。

换句话说就是,如果某个文件中声明变量、方法、类型别名、接口等都没有加上 import/export ,那么,这些声明将被视为全局可见。

基本使用

命名空间使用 namespace 关键字来定义,它会建立一个容器,内部的所有变量、方法、类型别名、接口等成员,都必须在这个容器里面使用。

A.ts 文件:

const name = '橙某人';
function fn() {};

B.ts 文件:

namespace BModule {
  const name = '橙某人';
  function fn() {};
}

同个文件也可以有多个命名空间,它们彼此相互独立隔离。

B.ts 文件:

namespace BModule {
  const name = '橙某人';
  function fn() {};
}
namespace BModuleType {
  type Person = {};
  interface Student {};
}
namespace BModuleClass {
  class Person {}
}

模块划分好后,接下来就要探究一下它的使用情况了,如何来导出导入使用。

导出

命名空间以外的地方要使用容器内部的成员,必须要在成员前面加上 export 关键字。

namespace BModule {
  export const name = '橙某人';
  function fn() {};
}

console.log(BModule.name); // ✅
BModule.fn(); // ❌

注意❗千万别把它和 ES6export/export default 搞混了,两者不是同一个东西。

我们可以执行 tsc 命令,看看编译后的代码:

var BModule;
(function (BModule) {
  BModule.name = '橙某人';
  function fn() { };
})(BModule || (BModule = {}));

命名空间内的 export 关键字经过编译后,完全不存在了,而 namespace 关键字的本质就是一个局部变量。

导入

如果要在其他文件中导入命名空间导出的成员,则需要使用三斜线指令。

B.ts 文件:

namespace BModule {
  export const name = '橙某人';
  function fn() {};
}

A.ts 文件:

/// <reference path="./B.ts" />

console.log(BModule.name); // ✅
BModule.fn(); // ❌

导出和导入就这样子,挺简单的吧。👻

不过,这还没完呢。🔉

其实 namespace 本身也能被整个导出,但是要注意,这个导出就是 ES6export 导出了❗❗❗而且它仅支持 export ,不支持 export default

B.ts 文件:

export namespace BModule {
  export const name = '橙某人';
  function fn() {};
}

A.ts 文件:

import { BModule } from './B.ts';

console.log(BModule.name); // ✅
BModule.fn(); // ❌

嵌套与重名合并

命名空间还能进行嵌套使用。

namespace BModule {
  export namespace BModuleInner {
    export const name = '橙某人';
  }
}

console.log(BModule.BModuleInner.name);

命名空间重名的时候也能自动进行重名合并。

namespace BModule {
  export const name = '橙某人'
}
namespace BModule {
  // export const name = 'yd' // ❌ 内部不可再有重名
  export const age = 18
}

console.log(BModule.name); // 橙某人
console.log(BModule.age); // 18


至此,本篇文章就写完啦,撒花撒花。

🚀🚀🚀Typescript通关秘籍(六)🔥🔥🔥

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

原文链接:https://juejin.cn/post/7313911078114967604 作者:橙某人

(0)
上一篇 2023年12月19日 上午10:21
下一篇 2023年12月19日 上午10:31

相关推荐

发表回复

登录后才能评论