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

基于TypeScript中对象的数组参数动态生成返回类型,typescript,typescript-typings,Typescript,Typescript Typings,我试图定义一个类型脚本定义,如下所示: 接口配置{ 字段名:字符串 } 函数foo(arr:T[]):记录 函数foo(arr:Config[]):记录{ 常量obj={} _.forEach(arr,条目=>{ obj[entry.fieldName]=未定义 }) 返回obj } const result=foo([{fieldName:'bar'}]) result.baz//这应该是错误的,因为数组参数不包含字段名为“baz”的对象。 上面的代码非常有用,这几乎正是我想要做的,只是我的

我试图定义一个类型脚本定义,如下所示:

接口配置{
字段名:字符串
}
函数foo(arr:T[]):记录
函数foo(arr:Config[]):记录{
常量obj={}
_.forEach(arr,条目=>{
obj[entry.fieldName]=未定义
})
返回obj
}
const result=foo([{fieldName:'bar'}])
result.baz//这应该是错误的,因为数组参数不包含字段名为“baz”的对象。
上面的代码非常有用,这几乎正是我想要做的,只是我的参数是一个对象数组而不是字符串数组

问题是
结果
的类型是
记录
,而我希望它是
记录
。我很确定我的
记录
的返回定义不正确(因为
T
是一个类似
Config
的对象数组),但我不知道如何用泛型类型正确地指定它


非常感谢您的帮助。

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

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

函数makeObject(s:T):{fieldName:T}{
返回{fieldName:s};
}
const result=foo([makeObject('bar')]))
//类型错误:类型记录上不存在属性“baz”
result.baz

我建议这样做

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
所以T[“fieldName”]是


等等。

这里的其他答案已经确定了这个问题:TypeScript将倾向于将字符串文字值的推断类型从其(如
“bar”
)扩展到
字符串。有不同的方法告诉编译器不要这样做,其中一些方法在其他答案中没有涉及

一种方法是
foo()
的调用者将
“bar”
的类型注释或断言为
“bar”
,而不是
字符串。例如:

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,这是
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>;
看起来不错。对
foo()
的正常调用现在输出
Record

如果这看起来像魔术,我同意。几乎可以这样解释:添加一个新的类型参数
s extends string
提示编译器应该推断出比
string
更窄的类型,因此
T extends{fieldName:s}
将倾向于推断为具有字符串文本
fieldName
的字段名。虽然
T
确实被推断为
{fieldName:“bar”}
,但
S
类型参数被推断为
字符串而不是
“bar”
。谁知道呢


我希望能够用一种更明显或更简单的方式来修改类型签名来回答这个问题;可能类似于
函数foo(…)
。事实上,不久前我提出了这个建议;到目前为止,还没有发生什么。如果您认为它会有用,您可能想去那里给它一个Linter,它永远不会“捕获”严格类型,因为
const obj={}
隐式地是
any
类型。因此,您将得到一个运行时错误
属性“baz”在类型记录上不存在
,但在
之后您无法严格获得它!实际上,我首先尝试了
S扩展字符串的方法,但没有成功;不过,我不记得我对你的解决方案做了什么不同。是的,我不怪你没有找到它;这很奇怪。这个签名和
函数foo(arr:{fieldName:s}[]):Record
之间有什么实际的区别吗?不,不是真的(除非
T
在实际用例中很重要,但是示例代码没有显示这种依赖性)。这是一个很好的建议,你可以在你的答案中随意提及(或者如果你愿意,我会将它添加到我的答案中),我认为它不适合我的答案,所以请在你的答案中随意使用它。
const resultConstAsserted = foo([{ fieldName: 'bar' } as const]);
resultConstAsserted.baz; // error as desired
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