【问题标题】:Dynamically generate return type based on array parameter of objects in TypeScript根据 TypeScript 中对象的数组参数动态生成返回类型
【发布时间】:2019-12-21 22:54:55
【问题描述】:

我正在尝试定义一个 TypeScript 定义,如下所示:

interface Config {
    fieldName: string
}
function foo<T extends Config>(arr: T[]): Record<T['fieldName'], undefined>
function foo(arr: Config[]): Record<string, undefined> {
  const obj = {}
  _.forEach(arr, entry => {
    obj[entry.fieldName] = undefined
  })

  return obj
}

const result = foo([{fieldName: 'bar'}])
result.baz // This should error since the array argument did not contain an object with a fieldName of "baz".

上面的代码受到Dynamically generate return type based on parameter in TypeScript 的高度启发,这几乎正是我想要做的,除了我的参数是一个对象数组而不是字符串数组。

问题是result 的类型是Record&lt;string, undefined&gt;,而我希望它是Record&lt;'bar', undefined&gt;。我很确定Record&lt;T['fieldName'], undefined&gt; 的返回定义不正确(因为T 是Config 类对象的数组),但我不知道如何使用泛型类型正确指定它。

非常感谢任何帮助。

【问题讨论】:

  • Linter 永远不会“捕获”严格类型,因为 const obj = {} 隐式属于 any 类型。所以,你会得到一个运行时错误Property 'baz' does not exist on type Record&lt;'bar', undefined&gt;,但是在.之后你不能严格得到它

标签: typescript typescript-typings


【解决方案1】:

这里的其他答案已经确定了这个问题:TypeScript 倾向于将字符串文字值的推断类型从其string literal type(如"bar")扩大到string。有不同的方法可以告诉编译器不要进行这种扩大,其中一些没有被其他答案所涵盖。

一种方法是foo() 的调用者将"bar" 的类型注释或断言为"bar" 而不是string。例如:

const annotatedBar: "bar" = "bar";
const resultAnnotated = foo([{ fieldName: annotatedBar }]);
resultAnnotated.baz; // error as desired

const resultAssertedBar = foo([{ fieldName: "bar" as "bar" }]);
resultAssertedBar.baz; // error as desired

TypeScript 3.4 引入了const assertions,foo() 的调用者无需显式写出类型即可请求更窄的类型:

const resultConstAsserted = foo([{ fieldName: 'bar' } as const]);
resultConstAsserted.baz; // error as desired

但所有这些都需要foo() 的调用者 以不同的方式调用它以获得所需的非扩展行为。理想情况下,foo() 的 类型签名 会以某种方式进行更改,以便在正常调用时自动发生所需的非扩展行为。

好吧,好消息是您可以做到这一点;坏消息是这样做的符号很奇怪:

declare function foo<
    S extends string, // added type parameter
    T extends { fieldName: S },
    >(arr: T[]): Record<T['fieldName'], undefined>;

首先让我们确保它有效:

const result = foo([{ fieldName: "bar" }]);
result.baz; // error as desired

看起来不错。对foo() 的正常调用现在输出Record&lt;"bar", undefined&gt;。

如果这看起来很神奇,我同意。 几乎可以通过说添加一个新的类型参数 S extends string 向编译器暗示它应该推断出比 string 更窄的类型来解释,因此 T extends {fieldName: S} 将倾向于被推断为带有字符串文字 fieldName 的字段名称。虽然T 确实被推断为{fieldName: "bar"},但S 类型参数被推断为string 而不是"bar"。谁知道。

我希望能够以更明显或更简单的方式来更改类型签名来回答这个问题;也许像function foo&lt;T extends { filename: const string}&gt;(...) 这样的东西。事实上,不久前我提交了microsoft/TypeScript#30680 来建议这个;到目前为止,还没有发生太多事情。如果您认为它有用,您可能想去那里并给它一个?。


最后,@kaya3 做了一个敏锐的观察:foo() 的类型签名似乎并不关心T 本身;它对T 所做的唯一事情就是查找fieldName 值。如果您的用例是这样,您可以通过只关心S 来大大简化foo() 签名:

declare function foo<S extends string>(arr: { fieldName: S }[]): Record<S, undefined>;

const result = foo([{ fieldName: "bar" }]);
result.baz; // error as desired

这看起来甚至不像是什么黑魔法,因为S 被推断为"bar"。


好的,希望这会有所帮助。祝你好运!

Playground link

【讨论】:

  • 太棒了!我实际上首先尝试了S extends string 方式,但无法让它工作;不过,我不记得我对您的解决方案做了什么不同。
  • 是的,我不怪你没找到;这很奇怪。
  • 这个签名和function foo&lt;S extends string&gt;(arr: { fieldName: S }[]): Record&lt;S, undefined&gt;有什么实际区别吗?
  • 不,不是真的(除非T 在实际用例中很重要,但示例代码没有显示这种依赖关系)。这是一个很好的建议,您应该在回答中随意提及(或者如果您愿意,我会将其添加到我的)
  • 我认为它不适合我的答案,所以请随意在你的答案中使用它。
【解决方案2】:

问题是对象字面量{fieldName: 'bar'} 被推断为类型{fieldName: string},而不是更具体的类型{fieldName: 'bar'}。您对对象所做的任何事情,例如将其放入数组并将其传递给泛型函数,都无法从其类型中恢复字符串文字类型'bar',因为该字符串文字不是其类型的一部分首先。

解决此问题的一种方法是使用通用函数而不是对象字面量来构造对象,这样可以保留更严格的 fieldName 属性类型:

function makeObject<T>(s: T): { fieldName: T } {
    return { fieldName: s };
}

const result = foo([ makeObject('bar') ])

// type error: Property 'baz' does not exist on type Record<'bar', undefined>
result.baz

Playground Link

【讨论】:

    【解决方案3】:

    我会提出类似的建议

    interface Config {
        fieldName: string;
    }
    function foo<T extends Config>(arr: ReadonlyArray<T>) {
        const obj = {} as { [P in T["fieldName"]]: undefined };
        arr.forEach(entry => {
            obj[entry.fieldName] = undefined;
        });
    
        return obj;
    }
    
    const result = foo([{ fieldName: "bar" }, { fieldName: "yo" }] as const);
    result.baz; // error
    

    关键是使用你的数组作为 const 来防止类型的任何扩大,以便泛型 foo 推断出正确的 T,更详细地说,T 的类型现在是

    { readonly fieldName: "bar" } | { readonly fieldName: "yo" }
    

    所以 T["fieldName"] 是

     "bar" | "yo" 
    

    等等

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-13
      • 2022-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多