为什么此代码片段的 TypeScript 无效?

发布于 2025-01-10 12:34:50 字数 1805 浏览 0 评论 0原文

考虑以下 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,我收到以下错误:

类型“string”不可分配给类型“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,第一个片段无效,而其他两个片段则很好,这一事实让我感到非常困惑。看起来类型推断在第一个片段中以某种方式失败了。它们不应该都是等价的吗?

Consider the following TypeScript snippet:

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())

When trying to compile this snippet in VS Code or in the TypeScript playground, I get the following error:

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

Which I don't really understand. What's really surprising is that moving the content of the return statement to a variable fixes the issue:

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());

Or explicitly setting the callback arguments types:

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());

I'm very confused by the fact that the first snippet is invalid while the two others are fine according to TypeScript. It looks like type inference is somehow failing in the first snippet. Shouldn't they all be equivalent?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

话少情深 2025-01-17 12:34:50

TS4.7+ 的更新

这是 TypeScript 的设计限制;有关详细信息,请参阅 microsoft/TypeScript#46707microsoft/TypeScript#48380 已修复 TypeScript 4.7。


TS4.6 的先前答案 -

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

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


microsoft/TypeScript#29478 中实现了一个普遍有用的功能,允许编译器使用generic 函数的预期返回类型 上下文 将上下文类型传播回泛型函数的参数。这可能没有多大意义,但它看起来像这样:

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已经注释"x" | 类型“y”,这为编译器提供了一些上下文,表明我们希望调用 foo(["x"]) 时的 T 类型更窄比 string ,因此它将其推断为字符串 字面意思输入 “x”

这种行为在 TypeScript 中有很多积极的影响,但也存在一些问题。你遇到了问题之一。


数组的 TypeScript 类型.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;
}

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

您需要的是第三个通用调用签名。让我们想象一下这是编译器选择的。在调用 reduce() 时,编译器期望返回类型为 boolean。因此,泛型类型参数 U 具有一个 boolean 上下文,并且该上下文类型按照 ms/TS#29478 传播回 init 参数。编译器会查看 init 参数是否为 true,并推断其为文字 true 类型,而不是 boolean

这是第一个问题,因为你的回调返回 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(...)

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


编译器不仅会尝试通用调用签名,还会尝试通用调用签名。它还尝试非泛型,其中 init 预计与数组元素具有相同的类型。这一次也失败了,这次有充分的理由,因为这意味着 init 必须是 string,并且回调返回一个 string< /code>,并且 reduce() 返回一个 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'. */

一般来说,当所有重载失败时,编译器会选择最早使用的一个。不幸的是,这意味着返回 string 的调用签名是编译器决定发生的。因此,return 语句显然是在 containsWord() 函数中返回一个 string,即 注释以返回布尔值。这就是导致您困惑的错误的原因:

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

奇怪!


<一href="https://www.typescriptlang.org/play?ts=4.5.4#code/JYOwLgpgTgZghgYwgAgIJSnAngHgCoB8yA3gLA BQyVyA5hCAEoQAmArkjgKoEAUCARgC5kPAA5QIAN2GcanMgSsMwvPODMAHsJCsAtv2jy4y5HgDaAXQCUyALxE5yUMDAyrMg NwVqyEAHsQAHF6JjYkPiERcSkVeUUTVSdNbT0DKCMEyxt7UzUQFxV3Uy9yAF8KdEwsADpxPzB6rFEIarpGFnYUWzQMbFqo esbm6okwiBLKvrqGsCaW-yCQjqQ7Hqr+wdnh0c6SihhWEAQwYACFALA4UABnAHU-KGYeIv4-PwAbCDgQEm9qBAC1zAyAO73 eeAgGlcyCBUFANEsqzMAHI8AALYDXJxYuAw+IQI4oADuLjReMk9GQRIezGuyIse0o-0BwOpj2EsPhq2RbOYyMZPk+wMirw +XX+3VB4MhYFaSzGPD+PioPEQCHk13x9CQ2SIauQADIDXjwNqWqAEO9WMwINceLyrPIAPRO5DQAZQACESuVLuQAFpA0Hg8G IAA-CORqPRmOxiM+nx+0Wfb7Y3z1ZBwa7XYA0EBwfifZANYtQVgQBPUMBlitMqhWEqJgBUIl0EDAaL8zBsk1wnJANAI1UgQ NCNRw1fLvE-0iYgk0lL5biSigHor8LUyV8qUMmZM-YR1jsRENEHKM6ozmhZ5nRTPyCbTooPv4qylEKh1QWwXaCuQfrug83 p1r6ro4IGMIbgOab+MCWY5nmBZFiWybipeqoIOqJoEjqJ6ZlhhrGpqpqEtUFpWjadoOs6rpAV6l5+iGzFBuGcbsRx8agYm rpoammLpvB2a5vmhYoCWh6XmeDY+k6LY8G2HZdj2vR9tBg7fgEv5juEl4CMI84xFBcIDiuB7qVuWg7voe7GGuxnwlk+GHhe 3HUNe64mTQlZFIej7PuQL6gRIYBKD8ipuVQH4yiMywQDwmHYSRuEQLqBEIEROFmuRRyUba9o0o6S6pT6MmgXJyAAHJ+Mgf GULA7x+HAzDILocBgAgaK2sWGJYggcBgtUfwAPL1Y1zXIAAjLVMDIAAzPIyIRAZ0SLi5Cirp5m5JFZOg2ek+5VFTA5OTk60 eQ5A6+epyLyDQcAUj1KAwB8jUkjB9FDaBeBzMgyJ8SAyKwRmCEich4k1VsKDIoeyJfVQo3QONLUAEwzfNi3LVEC7CGeZn2 ed257WkGRHZdR5pXjTj5DeNb3jWt20A94ldSCr1+O9NBuhgDzw9QP3NH9ANAwJcGZsJSFicWkO-ciZ5wzwKMAOwAGwAJw2E +FDlIF5AAiA1xitUjU0HwFxXAb9yPM8VhBfshzHKcPwvX4+C8FZZh4NYKgkMgIVhcgGhmaAADBYyA6xQ+tAsg-DGO+bw8GYA BEGhJ9YJR+gcRwnGcLs4Ie7vCGYh7e+TQVR8KcAAF7CCnSfIAAPsgSdYPXkoJ8nqfpxQmcOznztvDgdeF8gndp0UddBUAA" rel="nofollow noreferrer">Playground 代码链接

UPDATE FOR TS4.7+

This was a design limitation in TypeScript; see microsoft/TypeScript#46707 for details. It was fixed for TypeScript 4.7 by microsoft/TypeScript#48380.


PREVIOUS ANSWER FOR TS4.6-

This is a design limitation in TypeScript; see microsoft/TypeScript#46707 for details. It's sort of a cascading failure that results in some weird errors.

This is a design limitation in TypeScript; see microsoft/TypeScript#46707 for details. It's sort of a cascading failure that results in some weird errors.


A generally useful feature was implemented in microsoft/TypeScript#29478 allowing the compiler to use the expected return type of a generic function contextually to propagate a contextual type back to the generic function's arguments. That might not make much sense, but it sort of looks like this:

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"

Note how the type of bar is string, since the call to foo(["x"]) infers the type parameter T as just string. But baz has been 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".

This behavior has a lot of positive effects in TypeScript, and a few problems. You ran into one of the problems.


The TypeScript typings for Array.prototype.reduce() look like this:

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;
}

That's an overloaded method, with multiple call signatures. The first one is not relevant, since it doesn't have an init parameter. The second one is non-generic, and expects the accumulator to be of the same type as the array elements. The third one is generic and lets you specify a different type, U, for the accumulator.

The one you want here is that third, generic call signature. Let's imagine that's the one the compiler chooses. In your call to reduce(), the compiler expects the return type to be boolean. Therefore the generic type parameter U has a boolean context, and that contextual type propagates back to the init parameter as per ms/TS#29478. The compiler looks at the init parameter being true and infers it to be the literal true type instead of boolean.

Which is the first problem, because your callback returns acc && sentence.includes(word), which is boolean and not necessarily 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 */

This is the error you would see if reduce() only had the one call signature. And maybe from this it would be at least vaguely intuitive to employ a the workaround of writing true as boolean for init, or manually specifying U to be boolean like fullText.reduce<boolean>(...).

But unfortunately other stuff is going on which makes the error less obvious.


The compiler doesn't only try the generic call signature; it also tries the non-generic one, where init is expected to be the same type as the array elements. This one also failed, and this time for good reason, since it would mean that init would have to be a string, and that the callback returns a string, and that reduce() returns a string, which is wrong in a bunch of ways:

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 */

The compiler tries both overloaded call signatures and they both fail. If you look carefully at the error messages, the compiler is complaining about both of them:

/* 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'. */

Generally speaking when all overloads fail, the compiler picks the earliest one to use. And that unfortunately means that the call signature that returns string is what the compiler decides happens. So the return statement is, apparently, returning a string in your containsWord() function which is annotated to return a boolean. And that's what causes the error that's confusing you:

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

Bizarre!


Playground link to code

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文