keyof
keyof 是 TS
新增的一个运算符,它接受一个对象类型作为参数,会返回该对象类型的所有键名组成的联合类型。
javascript
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
的联合类型。
javascript
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
最常见的一个应用就是取出对象的某个指定属性的值。
javascript
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
有些区别,它能用来遍历出联合类型的每个成员类型。
javascript
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️⃣ 要求声明一个类型,它能获取数组/元组类型的第一项类型:
javascript
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️⃣ 要求声明一个类型,如果传入的类型参数是数组类型,就返回数组元素的联合类型,否则传入什么类型参数就返回什么类型:
javascript
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
中,我们如何去描述数组每一项的类型呢?答案是元组。
一般如下定义:
javascript
let arr: [string, number, boolean] = ['橙某人', 18, true];
// or
let arr = ['橙某人', 18, true];
用类型别名和接口声明的形式呢?(数组也是对象)如下:
javascript
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]
来获取匹配数组元素的联合类型,这是一个结论。
javascript
type ArrType1 = Array<string>;
type ArrType2 = [string, number, boolean];
type A1 = ArrType1[number]; // number
type A2 = ArrType2[number]; // string | number | boolean
当然,可能有小伙伴注意到,不是还有一个 length
属性吗?这是一个字符串索引,那会不会还有 T[string]
形式呢?
那当然是没有的。😐
javascript
type ArrType3 = [string, number, boolean];
type A3 = ArrType3[string]; // ❌ Type 'ArrType3' has no matching index signature for type 'string'
至于为什么会有这种奇异的情况呢?可以看看这个解释。传送门
等等❗❗❗ 小编是不是讲跑题了?我们不是在说 infer
运算符吗?呃......不要在意这些细节。😄
其实花点时间讲 T[number]
的意思,是为了做一下铺垫,下面我们直接把它干掉,换成 infer
看看效果。
javascript
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
运算符。
字符串
javascript
type getName<T> = T extends `我的名字叫${infer U}` ? U : '不知名';
type Name1 = getName<'我的名字叫橙某人'>; // 橙某人
type Name2 = getName<'掘金'>; // 不知名
对象
javascript
type getName<T> = T extends { name: infer U; } ? U : unknown;
type Name1 = getName<{ name: string }>; // string
type Name2 = getName<{ age: number }>; // unknown
函数
javascript
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
文件:
javascript
interface Person {
name: string;
age: number;
}
再创建 Student.ts
文件:
javascript
/// <reference path="./Person.ts" />
let student: Person = {
name: '橙某人',
age: 18
}
三斜线指令,咱只要认识它的三个指令即可。
-
/// <reference path ="..."/>
:用于引入我们自己编写的.ts
文件或者.d.ts
的声明文件。 -
/// <reference types="..." />
:用于引入第三方声明文件。
javascript
/// <reference types="@types/node" />
// 它表示会引入 `node_modules/@types/node/index.d.ts` 的声明文件。
/// <reference lib="..." />
:用于引入TS
内置的声明文件。
javascript
/// <reference lib="es2020" />
/// <reference lib="esnext.asynciterable" />
/// <reference lib="esnext.intl" />
/// <reference lib="esnext.bigint" />
TS
内置的声明文件一般我们不手动引入,普遍都是通过 tsconfig.json
文件进行配置。
json
{
"compilerOptions": {
"lib": [
"es2020", "es2019.array"
]
},
}
命名空间 - namespace
命名空间(namespace
)是 TS
为了模块化格式发明的一个新玩意,它源自 JS
中的闭包概念,它的作用一样是为了避免全局作用域被"污染"。
不过,命名空间在 ESM
模块广泛使用后,官方已经不推荐使用了。
但是呢,我们应该也要有所了解才行哦。😲
像有时,咱们可能在 A.ts
文件中声明:
javascript
const name = '橙某人';
function fn() {};
然后继续在 B.ts
文件中声明:
javascript
const name = '橙某人'; // ❌
function fn() {}; // ❌
本来按 ES6
的单文件模块形式,这样子是被允许的。
可惜,这却在 TS
中行不通了,如果我们期望某个成员声明仅仅作用于局部的作用域,这会就需要我们使用命名空间了。
这是因为在
TS
中,如果一个文件中不包含export
语句,那它就是一个全局的脚本文件。相应地,任何包含import
或export
语句的文件,就是一个模块(module
)。换句话说就是,如果某个文件中声明变量、方法、类型别名、接口等都没有加上
import/export
,那么,这些声明将被视为全局可见。
基本使用
命名空间使用 namespace
关键字来定义,它会建立一个容器,内部的所有变量、方法、类型别名、接口等成员,都必须在这个容器里面使用。
A.ts
文件:
javascript
const name = '橙某人';
function fn() {};
B.ts
文件:
javascript
namespace BModule {
const name = '橙某人';
function fn() {};
}
同个文件也可以有多个命名空间,它们彼此相互独立隔离。
B.ts
文件:
javascript
namespace BModule {
const name = '橙某人';
function fn() {};
}
namespace BModuleType {
type Person = {};
interface Student {};
}
namespace BModuleClass {
class Person {}
}
模块划分好后,接下来就要探究一下它的使用情况了,如何来导出导入使用。
导出
命名空间以外的地方要使用容器内部的成员,必须要在成员前面加上 export
关键字。
javascript
namespace BModule {
export const name = '橙某人';
function fn() {};
}
console.log(BModule.name); // ✅
BModule.fn(); // ❌
注意❗千万别把它和 ES6
的 export/export default
搞混了,两者不是同一个东西。
我们可以执行 tsc
命令,看看编译后的代码:
js
var BModule;
(function (BModule) {
BModule.name = '橙某人';
function fn() { };
})(BModule || (BModule = {}));
命名空间内的
export
关键字经过编译后,完全不存在了,而namespace
关键字的本质就是一个局部变量。
导入
如果要在其他文件中导入命名空间导出的成员,则需要使用三斜线指令。
B.ts
文件:
javascript
namespace BModule {
export const name = '橙某人';
function fn() {};
}
A.ts
文件:
javascript
/// <reference path="./B.ts" />
console.log(BModule.name); // ✅
BModule.fn(); // ❌
导出和导入就这样子,挺简单的吧。👻
不过,这还没完呢。🔉
其实 namespace
本身也能被整个导出,但是要注意,这个导出就是 ES6
的 export
导出了❗❗❗而且它仅支持 export
,不支持 export default
。
B.ts
文件:
javascript
export namespace BModule {
export const name = '橙某人';
function fn() {};
}
A.ts
文件:
javascript
import { BModule } from './B.ts';
console.log(BModule.name); // ✅
BModule.fn(); // ❌
嵌套与重名合并
命名空间还能进行嵌套使用。
javascript
namespace BModule {
export namespace BModuleInner {
export const name = '橙某人';
}
}
console.log(BModule.BModuleInner.name);
命名空间重名的时候也能自动进行重名合并。
javascript
namespace BModule {
export const name = '橙某人'
}
namespace BModule {
// export const name = 'yd' // ❌ 内部不可再有重名
export const age = 18
}
console.log(BModule.name); // 橙某人
console.log(BModule.age); // 18
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。