根据TypeScript中对象的数组参数动态生成返回类型

时间:2019-12-21 22:54:55

标签: typescript typescript-typings

我正在尝试定义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<string, undefined>时,其类型为Record<'bar', undefined>。我很确定Record<T['fieldName'], undefined>的返回定义是不正确的(因为T是一个类似Config的对象的数组),但我不知道如何正确地使用通用类型。

非常感谢您的帮助。

3 个答案:

答案 0 :(得分:2)

这里的其他答案已经确定了这个问题: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 assertionsfoo()的调用者可以使用这种方式来请求更窄的类型,而不必显式地写出该类型:

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

但是所有这些都需要foo() caller 来以不同的方式调用它以获得所需的非扩展行为。理想情况下,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<"bar", undefined>

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

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


最后,@ kaya3做出了一个敏锐的观察:foo()的类型签名实际上似乎并不在乎T本身。它对T所做的唯一一件事就是查询fieldName的值。如果您的用例是正确的,则只需关心foo(),就可以大大简化S签名:

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

答案 1 :(得分:0)

问题在于对象文字{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

答案 2 :(得分:0)

我会提出类似的建议

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"