在TypeScript中,为什么在函数类型上使用|和&运算符时它们的含义会反转?

5

在这份代码中,example1example2 让我感到困惑。

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;

//   re: example 1 and 2: 
//   After the =, | means "or" and & means "and"
//   Before the =, & means "or" and | means "and"
const example1: F1 & F2 = (a: string | number, b: string | number) => {}
example1("Hello", "World")
example1(1, 2)
// example1("Hello", 2) // Error! number is not assignable to parameter of type string... (and vice versa)

const example2: F1 | F2 = (a: string | number, b: string | number) => {}
// example2("Hello", "World") // Error! Argument of type string is not assignable to parameter of type never
// example2(1, 2) // Error! Argument of type number is not assignable to parameter of type never
// example2("Hello", 2) // Error! Argument of type string is not assignable to parameter of type never

//   re: example 3,4,5:
//   Before the =, | means "or"
// const example3: number | string = true // Error! Type Boolean is not assignable to type number | string
const example4: number | string = 1
const example5: number | string = "foo"

//   re: example 6,7
//   Before the =, & means "and"
// const example6: {a: string} & {b: number} = {a: "foo"} // Error! Type '{ a: string; }' is not assignable to type '{ a: string; } & { b: number; }'. 
// Property 'b' is missing in type '{ a: string; }' but required in type '{ b: number; }'.
const example7: {a: string} & {b: number} = {a: "foo", b: 5}

在我的看法中,example1example2中的运算符(等号前面的)似乎与其他运算符的行为相反。以下是我预期这些示例应该如何工作的方法:
const example1: F1 & F2 = (a: string | number, b: string | number) => {}
// example2("Hello", "World") // Error! Argument of type string is not assignable to parameter of type never
// example2(1, 2) // Error! Argument of type number is not assignable to parameter of type never
// example2("Hello", 2) // Error! Argument of type string is not assignable to parameter of type never

const example2: F1 | F2 = (a: string | number, b: string | number) => {}
example1("Hello", "World")
example1(1, 2)
// example1("Hello", 2) // Error! number is not assignable to parameter of type string... (and vice versa)

如果"字符串的类型 !== 数字的类型",那么example1甚至不编译也是有道理的。

为什么这个代码不能按照预期工作呢?


随意修改此标题。我相信它可以更好。 - Daniel Kaplan
3个回答

2

使用这些类型时,

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;

还有这个example1的声明,

const example1: F1 & F2 = (a: string | number, b: string | number) => {}
example1 声明了类型 F1 & F2,因此可以同时作为接受两个字符串和两个数字的函数进行调用。但是它不能接受混合类型的参数。你所分配给它的函数值可以,但是 F1 & F2 严格来说是 (a: string | number, b: string | number) => void 的超类型,因此当我们将其分配给具有静态超类型的变量时,我们会失去信息,就像将数字 3 分配给类型 unknown 的变量一样会失去信息。
const example2: F1 | F2 = (a: string | number, b: string | number) => {}
example2的类型是可以接受string类型参数或者number类型参数的函数。你所赋值的函数可以接受这两种类型的参数,所以这个赋值是正确的。
但我们永远也无法调用这个函数。我们需要传递两个参数,并且这两个参数要同时兼容F1F2的签名。 F1期望string类型参数,而F2期望number类型参数,因此我们需要传递既是字符串又是数字的东西,即string & number。而string & numbernever,即空类型。
第二个函数中|变成了&是由于一个称为variance的小问题。函数参数是反变的,因此((a: A1) => B1) & ((a: A2) => B2)等同于(a: A1 | A2) => B1 & B2,而((a: A1) => B1) | ((a: A2) => B2)等同于(a: A1 & A2) => B1 | B2。你可以阅读维基百科页面以了解更多有关其背后的数学原理,或者根据"and"和"or"类型表示法来理解。

example1声明了类型 F1 & F2,因此它既可以被调用作为接受两个字符串的函数,也可以被调用作为接受两个数字的函数。” 好的,但是为什么不能说 "example6 声明了类型 {a: string} & {b: number},因此它既可以被赋值为一个 {a: string} 对象,也可以被赋值为一个 {b: number} 对象?(const example6: {a: string} & {b: number} = {a: "foo"} 会报错)” - Daniel Kaplan
2
当您将值分配给交集类型的变量时,您分配给它的内容必须与两种类型兼容。当您使用变量时,可以自由地根据自己的意愿将其视为任一类型。对于联合类型,则相反。您可以随意分配任何一个类型,但在使用它时,必须考虑到两种可能性。这是控制的反转。在example1中,您正在谈论使用变量(即变量向您承诺),而在example6中,您正在谈论分配(即您向变量承诺)。 - Silvio Mayolo
我感觉我的问题的答案在这里,但是我很难理解你所措辞的方式。并没有冒犯之意,因为这不是非常具体的反馈,我很抱歉。也许如果你的解释和相关示例代码更加接近会有所帮助? - Daniel Kaplan

2

关于函数类型的参数,当这些函数组合在一起时,情况可能会令人困惑,这是事实。

@Bishwajitjha试图用集合论来解释它并不完全错误,尽管简单的图表可能已经足够了。

函数类型的交集 F1 & F2

Intersection of F1 and F2 sets

const example1: F1 & F2

example1函数可以同时被称为F1和F2。因此,它对应于一个TypeScript函数重载

  • 你可以像调用F1一样执行它(example1("Hello", "World")),
  • 或者像调用F2一样执行它(example1(1, 2)),
  • 但是你不能混合使用(example1("Hello", 2)会报错)

如果它们有返回类型,那么很容易看出TypeScript如何将F1 & F2交集作为一个重载处理:

// Overload demonstration
type F1b = (a: string, b:string) => string;
type F2b = (a: number, b:number) => number;

declare const example1b: F1b & F2b;

const r1b1 = example1b("foo", "bar") // string
parseInt(r1b1) // Okay
r1b1.toFixed() // Error! Property 'toFixed' does not exist on type 'string'

const r1b2 = example1b(1, 2) // number
parseInt(r1b2) // Error! Argument of type 'number' is not assignable to parameter of type 'string'.
r1b2.toFixed() // Okay

declare const r1b3: ReturnType<F1b & F2b> // When TS must choose, it uses the last overload => number
parseInt(r1b3) // Error! Argument of type 'number' is not assignable to parameter of type 'string'.
r1b3.toFixed() // Okay

declare const r1b4: ReturnType<F2b & F1b> // When TS must choose, it uses the last overload => string
parseInt(r1b4) // Okay
r1b4.toFixed() // Error! Property 'toFixed' does not exist on type 'string'

函数类型的联合 F1 | F2

Union of F1 and F2 sets

const example2: F1 | F2

这里更复杂,因为它不符合特定的TypeScript结构。

example2可能只是F1,或仅为F2,或两者的重载。因为联合类型没有提供任何进一步的信息,当我们尝试使用example2时,我们必须同时考虑所有这些可能性:https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#working-with-union-types

  • 作为F1,它需要将其参数都作为string
  • 作为F2,它需要将其参数都作为number
  • 作为F1 & F2,它可以具有两个都是string或都是number的参数(前面的部分)
因此,两个参数必须同时为字符串数字,即字符串&数字。这很不幸,正如@SilvioMayolo所描述的那样,这是从不的。
请注意,我们仍然可以对example2函数进行“有效”调用:
// Using never type...
declare const arg: string & number; // Inferred as never
example2(arg, arg); // Okay!.. but should never be reached

至少,返回类型(如果有的话)遵循直觉:

declare const r2b: ReturnType<F1b | F2b> // string | number
parseInt(r2b) // Error! Argument of type 'string | number' is not assignable to parameter of type 'string'. Type 'number' is not assignable to type 'string'.
r2b.toFixed() // Error! Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.

关于被分配的实际函数怎么样?

不幸的是,即使我们分配了一个可以接受更多种类参数类型的实际函数(它甚至可以是带有 booleannull、任意类型等联合类型;只要它可以被称为 F1 或者 F2 的一种方式或另一种方式调用),我们通过明确指定 example2 的类型失去了更加宽容的行为。

// Losing information of the actual function
const fn1d = (a: string | number | boolean, b: string | number | {a: number}) => {}
fn1d(true, {a: 1}) // Okay
const example1d: F1 & F2 = fn1d // Okay
example1d(true, {a: 1}) // Error! No overload matches this call. Overload 1 of 2, '(a: string, b: string): void', gave the following error. etc.

const example2d: F1 | F2 = fn1d // Okay
example2d(true, {a: 1}) // Error! Argument of type 'boolean' is not assignable to parameter of type 'never'.

F1 | F2 联合类型的情况下,如果我们分配了一个 更窄 的类型,例如 (a: string, b: string) => {},那么情况就会有所不同:在这种情况下,TypeScript 会遵循赋值,并推断出我们有一个更窄的类型(正如 https://www.typescriptlang.org/docs/handbook/2/narrowing.html#assignments 中所描述的)。
// Narrower type assignment
const example2c: F1 | F2 = (a: string, b: string) => {}
example2c("Hello", "World") // Okay
example2c(1, 2) // Error! Argument of type 'number' is not assignable to parameter of type 'string'.
example2c("Hello", 2) // Error! Argument of type 'number' is not assignable to parameter of type 'string'.

关于example3example7怎么样?

正如@SilvioMayolo的评论所指出的那样,这些示例展示了赋值的预期效果。

更进一步,我们可以描述它们的用法,但它们仍然接近直觉:

declare const example8: {a: string} & {a: number}
const a = example8.a // never
parseInt(a) // Okay... but should never be reached
a.toFixed() // Property 'toFixed' does not exist on type 'never'.

declare const example9: {a: string} | {a: number}
const a9 = example9.a // string | number
parseInt(a9) // Error! Argument of type 'string | number' is not assignable to parameter of type 'string'. Type 'number' is not assignable to type 'string'.
a9.toFixed() // Error! Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.

只有组合函数类型的参数才是真正具有误导性的。

TypeScript游乐场


那些图片真的很清晰易懂。如果你分配了一个更窄的类型,情况会有所不同。哦!好的,这帮了我很多。我想我过于关注 F1 <operator> F2 而忽略了其他。等号右侧似乎是导致我困惑的主要原因。 - Daniel Kaplan
“但是我确实分配了…”这个标题和接下来的段落让我感到困惑。其余部分非常有帮助,但是否可能使用不同的词语重写那些部分?我知道什么是方法覆盖(至少在其他语言中),但你使用它的方式让我无法理解。 - Daniel Kaplan
我明白这可能会让人感到困惑。我会尝试重新措辞。 - ghybs
你好。你已经编辑过这个了吗? - Daniel Kaplan
是的,请告诉我如果您仍然觉得那部分令人困惑。 - ghybs

0

由于类型无非是集合,因此它们都遵循集合理论。

首先,让我们了解一下类型系统中 & 的一个关键行为。

{ a: 'number' } & { b: 'string' } => { a: 'number', b: 'string' }

两个组成类型都被添加了,因为它们是独立的类型。

记住这一点,让我们尝试证明这种行为以及 F1 & F2F1 | F2 的等效结果应该是什么。

type F1 = (a: string, b:string) => void;
type F2 = (a: number, b:number) => void;



Using SET Theory

Eq 1.  (A | B) = A + B - (A & B) 
Eq 2.  (A & B) = A + B - (A | B) 

Also, our F1 and F2 are independent type sets, hence

Eq 3. F1 & F2 => F1 + F2


Case 1: F1 & F2

> F1 + F2     ...(By using eq 3)
> (string, string) => void + (number, number) => void 
> Basically means we can only call F1 and F2 with their own types (not their union one)

 

Case 2: F1 | F2


> F1 + F2 - (F1 & F2)    ....(By Using Eqn 1)
> F1 + F2 - (F1 + F2)    ....(By using Eqn 3)
> never (as everything cancels out)



1
非常抱歉我的理解力有些迟钝,但我认为集合论比所涉及的概念更加抽象(或许也更加复杂)。因此,用这些术语来解释并没有帮助我澄清我的理解。 - Daniel Kaplan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接