为什么对于扩展接口和接口交集的类型别名,泛型类型参数的推断方式不同?
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)
您在这里遇到的不是推理问题,而是side-effect 消除冗余交集成员的问题。注意 inp
类型中的 & Extra
。当 f
在调用点传递类型为 Ext2
的变量时,inp
的类型本质上变为 Base & Extra & Extra
.
由于从交集中剔除了相同的类型,inp
的类型实际上变成了Base & Extra
,然后类型参数T
被推断为Base
,因为它满足extends Base
。而且,实际上,如果您删除 T
与 Extra
的交集,您将观察到正确的推论:
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
这就是为什么当您将 Ext1
与 Extra
相交时,没有任何反应 — 没有要消除的相同类型,只有 Ext1
(一个 子类型Extra
的 )和 Extra
(超类型)。
在下面的玩具实验中(从真实示例简化而来),为什么根据模板是使用扩展类型还是使用相交类型实例化,泛型类型参数的推断方式不同?
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)
您在这里遇到的不是推理问题,而是side-effect 消除冗余交集成员的问题。注意 inp
类型中的 & Extra
。当 f
在调用点传递类型为 Ext2
的变量时,inp
的类型本质上变为 Base & Extra & Extra
.
由于从交集中剔除了相同的类型,inp
的类型实际上变成了Base & Extra
,然后类型参数T
被推断为Base
,因为它满足extends Base
。而且,实际上,如果您删除 T
与 Extra
的交集,您将观察到正确的推论:
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
这就是为什么当您将 Ext1
与 Extra
相交时,没有任何反应 — 没有要消除的相同类型,只有 Ext1
(一个 子类型Extra
的 )和 Extra
(超类型)。