2024 TypeScript温习杂记
特殊类型
- any
- 表示任何类型
- object
- 表示任何非原始值类型。与 JS 的 Object 不同,不是同一个东西,注意大小写。与
{}
类型也不同,{}
表示的是空对象类型。 - unknown
- 表示未知类型。跟 any 很类似,可以表示任何类型。但是
unknown
这个类型与 any 相反,无法进行任何操作。可以把它用来标注,告诉使用者,此值的类型未知,需要小心。 - never
- 表示不会发生的情况。比如,一个负责抛异常的函数,其返回值就是
never
,因为根本不可能有返回。never
只能赋值给never
自己,可以用来防止条件判断时,遗漏处理某个类型。
interface 场景下 & 与 extends 的区别
两种都是组合类型用的,区别在于处理类型冲突的时候,extend 会直接报错,而 &
会把冲突字段变为 unknown
。
训练习题
这个 Type Challenges Repo不错。
函数类型的兼容性
在做 Get Return Type 这道题的时候,在答案区发现
这个评论。说真的,挺难理解为什么要用 never
而不是用 any
的。
看了一通讲 TS 类型协变、共变的文章,还是云里雾里。思考后,自我总结一下,这其实是函数类型的兼容性问题。
先来看看变量的类型兼容:
// 字符串面量类型 '123' 是 string 的子类型,
// 所以下面这种变量赋值行为是兼容的
const a: string = '123' as const;
// 数字面量类型 123 跟 string 之间的类型是分叉的,它不是 string 的子类型
// 所以下面这种变量赋值是不兼容的
const b: string = 123 as const;
简单类型的兼容是很好理解的。这种兼容性表现在越特殊的,可以向通泛的"降级",二者之间要有 subtype
(子类型) 的关系。
那么"函数的兼容性"也就很好理解了。这理解其实也是很无感的,大家都司空见惯习以为常了。答案区的讨论之所以会看起来那么复杂,是因为其所讲的情况是少见的使用情况。
所谓"函数的兼容性"其实说得很模糊:
- 它到底是调用时参数的兼容性?
- 还是返回值的兼容性?
- 还是函数整体作为一个类型的兼容性?
前两者跟变量赋值其实没有什么区别。答案区说的那种情况,是把函数作为整个整体来分析的。
function f1(n: string) {
}
type TF1 = typeof f1;
function f2(n: '123') {
}
type TF2 = typeof f2;
// 我们声明 foo1 的类型是 (n: string) = void;
// 则调用 foo1 的时候,就可以随意传入一个 string,例如:
// foo1("789");
let foo1: TF1;
// 那么是否可以把 f2 赋值给变量 foo1 ?
// 调用 foo1 的只知道它的签名是 (n: string) -> void;
// 如果把 f2 赋值给 foo1,会导致被调用的时候,传入不符合
// f2 预期的参数,string 无法兼容 '123'。所以下面的赋值
// TS 是会报错的。
foo1 = f2;
那么回头看看 Get Return Type 这道题目,我们很容易就能想到这样的实现:
type MyReturnType<T> = T extends (...args: any[]) => infer RT ? RT : never;
这样的实现算是比较完美了,大部分情况下都是没问题的。但是,对于这样的函数,这个 MyReturnType
就无法获取返回值的类型:
function foo(a: string, b: never) {
return a;
}
type T = MyReturnType<typeof foo>; // never
为什么?因为 (a: string, b: never)
这样的参数签名,不是 (...args: any[])
的子类型。为了分析,我们可以把后者根据此场景转化为 (a: any, b: any)
。很显然,对于第一个参数 string
肯定可以兼容于 any
。关键是第二个参数,never
无法兼容于 any
。
这是因为 never
可以赋值给任何类型,而任何类型都不可以赋值给 never
(除了它自己)。也可以这么说,never
是所有其它类型的子类型,是 TS 的特殊类型。在 never
出现之前,这里用 any
当然是没有问题的,但是现在用 never
会更准确一点。
type MyReturnType<T> = T extends (...args: never[]) => infer RT ? RT : never;
上面的签名不要理解为:
T
需要是一个参数签名为(...args: never[])
的函数
而应该参照回调函数的场景,理解为:
MyReturnType 承诺用
(...args: never[])
这样的参数来调用你传入的回调函数T
也就是说,你的传入的回调函数 T
,只要能接受 (...args: never[])
这样的参数,那就进入 true 分支。试问什么样的函数无法接受这样的参数?答案是没有,因为 never
可以赋值给任意类型。这样是不是就好理解了呢?
那么用 unknown
呢?这么说吧,假设这些特殊类型之间存在继承关系,那么其继承关系大概是这样的:
never -> any -> {其它类型} -> unknown
unknow
是所有类型的 base。所以能不能用 unknown
,答案不言而喻。