..

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,答案不言而喻。