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

Jon*_*ins 4 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".
Run Code Online (Sandbox Code Playgroud)

上面的代码受到了 基于 TypeScript 中的参数动态生成返回类型的启发,这几乎正是我想要做的,除了我的参数是对象数组而不是字符串数组。

问题是 的类型resultRecord<string, undefined>我想要的类型Record<'bar', undefined>。我很确定我的返回定义Record<T['fieldName'], undefined>不正确(因为T是类似对象的数组Config),但我无法弄清楚如何使用泛型类型正确指定它。

任何帮助深表感谢。

jca*_*alz 7

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

一种方法是调用者对as和 notfoo()的类型进行注释或断言。例如:"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
Run Code Online (Sandbox Code Playgroud)

TypeScript 3.4 引入了const断言,这是一种让 的调用者foo()请求更窄类型的方法,而无需显式写出类型:

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

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

好消息是你可以做到;坏消息是这样做的符号很奇怪:

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

首先让我们确保它有效:

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

看起来不错。foo()对now 的正常调用输出Record<"bar", undefined>.

如果这看起来像魔法,我同意。这几乎可以解释为,添加一个新的类型参数S extends string提示编译器应该推断出一个比 更窄的类型string,因此T extends {fieldName: S}往往会被推断为带有字符串文字的字段名称fieldName。虽然T确实被推断为{fieldName: "bar"},但S类型参数被推断为stringand not"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
Run Code Online (Sandbox Code Playgroud)

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


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

游乐场链接