为什么此代码段无效 TypeScript?

Why is this snippet invalid TypeScript?

考虑以下 TypeScript 片段:

function containsWord(): boolean {
    const fullText: string[] = ['This is a sentence with seven words'];
    const word: string = 'word';

    return (
        fullText.reduce((acc, sentence) => acc && sentence.includes(word), true)
    );
}

console.log(containsWord())

尝试在 VS Code 或 TypeScript playground 中编译此代码段时,出现以下错误:

Type 'string' is not assignable to type 'boolean'

我不太明白。真正令人惊讶的是,将 return 语句的内容移动到一个变量就解决了这个问题:

function containsWord(): boolean {
    const fullText: string[] = ['This is a sentence with seven words'];
    const word: string = 'word';

    const result = fullText.reduce((acc, sentence) => acc && sentence.includes(word), true);

    return result;
}

console.log(containsWord());

或显式设置回调参数类型:

function containsWord(): boolean {
    const fullText: string[] = ['This is a sentence with seven words'];
    const word: string = 'word';

    return (
        fullText.reduce((acc: boolean, sentence: string) => acc && sentence.includes(word), true)
    );
}

console.log(containsWord());

根据 TypeScript,第一个代码段无效而其他两个代码段正常,这让我感到非常困惑。看起来类型推断在第一个片段中以某种方式失败了。他们不应该都是等价的吗?

这是 TypeScript 的设计限制;有关详细信息,请参阅 microsoft/TypeScript#46707。这是一种级联故障,会导致一些奇怪的错误。


microsoft/TypeScript#29478 allowing the compiler to use the expected return type of a generic function contextually 中实现了一个普遍有用的功能,用于将上下文类型传播回通用函数的参数。这可能没有多大意义,但它看起来像这样:

function foo<T>(x: [T]): T { return x[0] }

const bar = foo(["x"]);
// function foo<string>(x: [string]): string

const baz: "x" | "y" = foo(["x"]);
// function foo<"x">(x: ["x"]): "x"

请注意 bar 的类型是 string,因为对 foo(["x"]) 的调用将类型参数 T 推断为 string。但是 baz 已经 annotated to be of type "x" | "y", and this gives the compiler some context that we want the T type in the call to foo(["x"]) to be narrower than string, so it infers it as the string literal type "x".

此行为在 TypeScript 中有很多积极影响,但也存在一些问题。你运行陷入其中一个问题。


TypeScript typings for Array.prototype.reduce() 看起来像这样:

interface Array<T> {
  reduce(cb: (prev: T, curr: T, idx: number, arr: T[]) => T): T;
  reduce(cb: (prev: T, curr: T, idx: number, arr: T[]) => T, init: T): T;
  reduce<U>(cb: (prev: U, curr: T, idx: number, arr: T[]) => U, init: U): U;
}

这是一个 overloaded 方法,具有多个调用签名。第一个不相关,因为它没有 init 参数。第二个是 non-generic,并期望累加器与数组元素的类型相同。第三个是通用的,允许您为累加器指定不同的类型 U

您在这里想要的是第三个通用调用签名。让我们假设这是编译器选择的那个。在您对 reduce() 的调用中,编译器期望 return 类型为 boolean。因此泛型类型参数 U 有一个 boolean 上下文,并且该上下文类型根据 ms/TS#29478 传播回 init 参数。编译器将 init 参数视为 true 并将其推断为文字 true 类型而不是 boolean.

这是第一个问题,因为你的回调return是acc && sentence.includes(word),也就是boolean不一定是true:

let b: boolean = fullText.reduce(
    (acc, sentence) => acc && sentence.includes(word), // error!
    // --------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // boolean is not assignable to true
    true
);
/* (method) Array<string>.reduce<true>(
     cb: (prev: true, curr: string, idx: number, arr: string[]) => true, 
     init: true
   ): true */

如果 reduce() 只有一个调用签名,您将看到此错误。也许从这个角度来看,采用为 init 编写 true as boolean 的变通方法,或者手动将 U 指定为 boolean,如 fullText.reduce<boolean>(...) ].

但不幸的是,其他事情正在发生,这使得错误不太明显。


编译器不仅尝试通用调用签名;它还会尝试 non-generic 一个,其中 init 预计与数组元素的类型相同。这个也失败了,这次是有充分理由的,因为这意味着 init 必须是 string,而回调 return 是 string,并且reduce() return 是 string,这在很多方面都是错误的:

b = fullText.reduce( // error!
// <-- string is not assignable to boolean
    (acc, sentence) => acc && sentence.includes(word), // error!
    // --------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // boolean is not assignable to string
    true);

/* (method) Array<string>.reduce(
    cb: (prev: string, curr: string, idx: number, arr: string[]) => string, 
    init: string
  ): string */

编译器尝试了两个重载的调用签名,但都失败了。如果您仔细查看错误消息,编译器会抱怨它们:

/* No overload matches this call.
  Overload 1 of 3, '(cb: (prev: string, curr: string, idx: number, array: string[]) => string, 
    init: string): string', gave the following error.
    Type 'boolean' is not assignable to type 'string'.
  Overload 2 of 3, '(cb: (prev: true, curr: string, idx: number, array: string[]) => true, 
    init: true): true', gave the following error.
    Type 'boolean' is not assignable to type 'true'. */

一般来说,当所有重载都失败时,编译器会选择最早的一个来使用。不幸的是,这意味着 returns string 的调用签名是编译器决定的。所以 return 语句显然是 return 在你的 containsWord() 函数中 stringannotated 到 return 一个 boolean. 那是导致您感到困惑的错误的原因:

Type 'string' is not assignable to type 'boolean'.

奇怪!


Playground link to code