为什么对于扩展接口和接口交集的类型别名,泛型类型参数的推断方式不同?

Why is the generic type parameter inferred differently for an extended interface and for a type alias of an intersection of interfaces?

在下面的玩具实验中(从真实示例简化而来),为什么根据模板是使用扩展类型还是使用相交类型实例化,泛型类型参数的推断方式不同?

interface Base { b: number }
interface Extra { a: string }
interface Ext1 extends Extra { b: number }
type Ext2  = Base & Extra

// f returns a function that takes a T as input
const f = <T extends Base>(inp: T & Extra): ((arg: T) => void) => {
    return (arg: T) => console.log(inp.a + arg.b) 
}

const x1: Ext1 = { a: "x1", b: 1 }
const x2: Ext2 = { a: "y1", b: 2 } 

const f1 = f(x1) // T inferred to Ext1
const f2 = f(x2) // T inferred to Base, NOT Ext2 (why?)

const inp = { b: 3 }

// error Argument of type '{ b: number; }' is not assignable to parameter of type 'Ext1'. Property 'a' is missing in type '{ b: number; }' but required in type 'Ext1'.
const out1 = f1(inp) 

// ok since inp is of type Base
const out2 = f2(inp)

Playground Link

您在这里遇到的不是推理问题,而是side-effect 消除冗余交集成员的问题。注意 inp 类型中的 & Extra。当 f 在调用点传递类型为 Ext2 的变量时,inp 的类型本质上变为 Base & Extra & Extra.

由于从交集中剔除了相同的类型,inp的类型实际上变成了Base & Extra,然后类型参数T被推断为Base,因为它满足extends Base。而且,实际上,如果您删除 TExtra 的交集,您将观察到正确的推论:

interface Base { b: number }
interface Extra { a: string }
interface Ext1 extends Extra { b: number }
type Ext2  = Base & Extra

// f returns a function that takes a T as input
const f = <T extends Base>(inp: T): ((arg: T) => void) => {
    return (arg: T) => console.log(inp.a + arg.b)
}

const x1: Ext1 = { a: "x1", b: 1 }
const x2: Ext2 = { a: "y1", b: 2 } 

const f1 = f(x1) // T inferred to Ext1
const f2 = f(x2) // T inferred to Ext2

const inp = { b: 3 }

const out1 = f1(inp) // error
const out2 = f2(inp) // error

说完这些,让我们澄清一个小误解。从 interface 扩展的工作方式与 与交叉两个接口的工作方式类似 ,但它们 不同 extends 表示 left-hand 侧类型(接口)是 子类型 (或者,换句话说,是 narrower ),并且 right-hand 侧类型(接口)是 supertype(或者是 wider)。

相反,交集创建组合 类型。查看下面的示例,了解 extends& 之间的关键区别:

interface A { a: string, b: boolean }
interface B { a: number, b: boolean }
interface C extends A, B {} // error, cannot extend

type a = { a:string, b: boolean }
type b = { a:number, b: boolean }
type c = a & b; // no error, but 'a' is never

这就是为什么当您将 Ext1Extra 相交时,没有任何反应 — 没有要消除的相同类型,只有 Ext1(一个 子类型Extra)和 Extra超类型)。

Playground