再谈泛型-约束关系

453 阅读3分钟

本系列作为《TypeScript 入门实战笔记》课程(见拉勾教育)的补丁,随时更新。

约束关系

这篇文章源于与同事的关于泛型类型推断的讨论,如下示例所示(playground):

type Func = (p: any) => void

function foo<T extends Func>(p: T) {}

function bar<T>(p: T) {}

foo((p = 1) => {}) // F1
foo((p: number = 1) => {}) // F2

bar((p = 1) => {}) // B1
bar((p: number = 1) => {}) // B2

如第 10 讲泛型里提到过的,(函数)泛型入参缺省的时候(即缺少 <泛型入参>泛型,且未指定默认类型),会根据实参进行类型推断;所以在上面示例里,调用 foo 和 bar 函数的入参进行类型推断。

其中,注释 F2、B1、B2 的地方推断的入参类型都挺好理解,即 (p?: number) => void。但 foo 函数类型比 bar 多了一层约束关系(extends Func),它会约束 foo 函数入参的类型,但却没有约束入参(函数)类型的参数的类型(而是指定其类型是 any),所以注释 F1 推断的类型会是 (p?: any) => void

我们需要建立 foo 函数对入参(函数)类型入参类型的约束,比如给 Func 添加一个泛型入参:

type Func<P = any> = (p: P) => void

function foo<T extends Func>(p: T) {}

但这实际还是没有约束到参数的类型,我们需要进一步修改(playground):

type Func<P = any> = (p: P) => void

function foo<T>(p: Func<T>) {}

function bar<T>(p: T) {}

foo((p = 1) => {})

这样似乎就可以了,但实际推断出来的类型是 Func<unknown>,等价于 (p: unknown) => void。Interesting!(p = 1) => {} 作为一个函数整体来看,参数 p 的类型受默认值影响,会是(猜测是”类型缩小“,从 unknown 到 number) number;但在受到泛型入参约束的时候,似乎是被作为个体对待,直接拿到了明确的类型 unknown。这样就可以解释的清了(具体是不是这样,估计就要扒一下官方源码了,另外一个解释可以说是泛型入参缺省情况下是 unknown)。所以,foo 函数肯定无法把入参 (p = 1) => {} 类型推断为 (p?: number) => void 或者 (p: number | undefined) => void

此外大原则上,泛型入参和受它约束的入参,在做类型推断时,谁更明确(specified),入参的类型就受谁影响(但如果显式指定泛型入参,那么就不用类型推断了),如以下示例(plaground),受泛型入参默认类型影响,当入参类型不明确的时候,F1、B1,都推断出一个 any。

type Func<P = any> = (p: P) => void

function foo<T = any>(p: Func<T>) {}

function bar<T = any>(p: T = (() => void 0) as any as T) {}

foo((p = 1) => {}) // F1
foo((p: number = 1) => {})

bar() // B1
bar((p: number = 1) => {})

无效约束的妙用

见示例 playground

type Func = <P>(p: P) => void

function foo<T extends Func>(p: T) {}

function bar<T>(p: T) {}

foo((p = 1) => {}) // ts(2322)
foo((p: number = 1) => {}) // ts(2345)

因为 Func 对应泛型的入参 P,并未真正约束到 foo 函数泛型入参(函数类型)T 的参数的类型,所以对 foo 的两次调用都会提示类型错误,因为 P 可能是任意类型。这就有意思了,比如可以用来实现 isEqual([playground]www.typescriptlang.org/play?#code/…

type isEqual<A, B> = (<P>() => P extends A ? 1 : 2) extends (<P>() => P extends B ? 1 : 2) ? true : false;
type E1 = isEqual<never, any>;
type E2 = isEqual<any, any>;

如以上示例所示,当且仅当 A 和 B 入参完全一致的时候,extends 成立(more issue)。