打字稿:在函数的返回值中,“字符串”类型不能分配给“从不”类型

时间:2019-10-24 10:04:23

标签: typescript

基本上,我很感兴趣这个示例为何起作用:

interface Alpha {}
interface Beta {}

interface HelloMapping {
  'Alpha': Alpha
  'Beta': Beta  
}

const hello = <T extends keyof HelloMapping>(arg: T): HelloMapping[T] => 
{
  if (arg === 'Alpha') return {} as Alpha;
  return {} as Beta;
};

const a = hello('Alpha') // a is of interface Alpha

但这不是:

interface HelloMapping2 {
  'Alpha': string
  'Beta': number
}

const hello2 = <T extends keyof HelloMapping2>(arg: T): HelloMapping2[T] => 
{
  if (arg === 'Alpha') return "42" as string
  return 42 as number
};

函数代码第一行中的Type 'string' is not assignable to type 'never'错误。 唯一的区别是将接口更改为stringnumber类型

请注意,我知道如何使用函数重载来实现该功能,但我很好奇为什么它不起作用。

2 个答案:

答案 0 :(得分:2)

第一个起作用的原因是@zerkms指出的原因:AlphaBeta是同一类型。 TypeScript的类型系统是structural,不是标称系统。如果AlphaBeta的形状相同(即the empty type {}),则它们的类型相同,尽管名称不同。因此,让我们忽略该示例,因为它基于意外的类型标识。


第二个示例不起作用的原因是generics do not get narrowed by control flow analysis。校验arg === 'Alpha'不能使编译器确信T现在是'Alpha'。这样做通常是不明智的。如果存在类型T的多个可能值,则检查类型T的一个值不一定对其他任何值都有影响。

尽管提出了可能解决该问题的建议,但目前尚无干净的解决方案。一种建议是允许您指定一个通用类型must be exactly one literal member of a union,这样存在的所有类型T的值都必须相同,并且可以在缩小一个值时安全地缩小所有值。

如果您希望看到这种解决方案的实现或解决的情况,则可能需要直接解决这些GitHub问题,并给他们一个?或描述您的用例(如果它特别引人注目)。

那么,与此同时该怎么办?


总是可以使实现正确编译的答案是使用type assertionsoverloads(它们被我称为“道德上”的等效项,因为重载本质上是断言了参数的类型)并返回实现签名中的那些)。在您的情况下,它们看起来像这样(我知道您知道如何做到这一点,但后来的其他人可能不会这样做):

const helloAssert = <T extends keyof HelloMapping>(arg: T): HelloMapping[T] => {
    if (arg === 'Alpha') {
        return "42" as HelloMapping[T];
    }
    return 42 as HelloMapping[T];
};

function helloOverload<T extends keyof HelloMapping>(arg: T): HelloMapping[T];
function helloOverload(arg: keyof HelloMapping) {
    if (arg === 'Alpha') {
        return "42"
    }
    return 42;
}

尽管如此,它们有些不安全,因为尽管它们捕获了完全疯狂的错误(例如,return true),但它们并没有捕获简单的混淆(例如,将===更改为{{1} })。


但是,有时您可以通过放弃控制流分析来避免这些不安全的解决方案,而直接表示通用类型操作:类型为!==的对象o和类型为键的O即使kK是通用的,o[k]也会产生类型为O[K]的可读属性O

K

因此,您将一个实际的const helloMap = <T extends keyof HelloMapping>(arg: T): HelloMapping[T] => ({ Alpha: "42", Beta: 42 })[arg]; 实例赋予HelloMapping并建立索引。当然,这可能不适用于所有用例,但有时以一种与编译器一起工作而不是与之相反的方式来做事很有用。


好的,希望能有所帮助;祝你好运!

Link to code

答案 1 :(得分:0)

正如@zerkms在其评论中正确指出的那样,第一个示例仅起作用,因为接口AlphaBeta为空,并且它们的交集(这是函数的返回类型)也为空对象。一旦您尝试向其中任何一个添加属性,它将立即停止工作。因此,实际上,这并不比第二个示例更好。

此问题的上下文是以下答案: https://stackoverflow.com/a/54443711/2874705

我真的很喜欢使用映射来从函数中很好地推断出返回值的想法。可能需要一些调整:

interface Alpha {
    a: boolean
}
interface Beta {
    b: string
}

interface HelloMapping {
    'Alpha': Alpha
    'Beta': Beta
}

const helloValues: HelloMapping = {
    'Alpha': {
        a: true
    },
    'Beta': {
        b: '42'
    }
}

const hello = <T extends keyof HelloMapping>(arg: T) => {
    return helloValues[arg]
};

const a = hello('Alpha') // a has interface Alpha

另一个想法是像上面我链接的答案一样使用函数重载。