我想创建一个对象,其中的值都是同一泛型的不同实例。然后,我想动态地键入对象以获取和使用该值,所有这些都以类型安全的方式进行。
该对象旨在包含多个 100 个元素,因此手动键入会很不方便,而是希望推断类型。
这是我想要实现的目标的一个示例。这是一个人为的例子,我试图删除与这个特定问题无关的任何内容。
type Items = {
a: string;
b: number;
c: number;
d: string;
};
const createTemplate = <T extends Record<string, string | number>>(
opts: {
pick: (items: Items) => T;
use: (props: T) => string;
},
) => opts;
const FirstTemplate = createTemplate({
pick: (items) => ({ foo: items.a, bar: items.b, }),
use: (props) => `${props.foo} and ${props.bar}`,
});
const SecondTemplate = createTemplate({
pick: (items) => ({ foo: items.c, bar: items.d, }),
use: (props) => `${props.foo} or ${props.bar}`,
});
const TEMPLATE_MAP = {
One: FirstTemplate,
Two: SecondTemplate,
}
const evaluateTemplate = (items: Items, key: string) => {
const template = TEMPLATE_MAP[key];
// ^? const template: any
if (template === undefined) {
throw new Error("Template not found")
}
const picked = template.pick(items)
const evaluated = template.use(picked)
return evaluated
}
不幸的是,打字稿 lsp 在我尝试键入
TEMPLATE_MAP
的行上显示错误。
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ One: { pick: (items: Items) => { foo: string; bar: number; }; use: (props: { foo: string; bar: number; }) => string; }; Two: { pick: (items: Items) => { foo: number; bar: string; }; use: (props: { foo: number; bar: string; }) => string; }; }'.
No index signature with a parameter of type 'string' was found on type '{ One: { pick: (items: Items) => { foo: string; bar: number; }; use: (props: { foo: string; bar: number; }) => string; }; Two: { pick: (items: Items) => { foo: number; bar: string; }; use: (props: { foo: number; bar: string; }) => string; }; }'.(7053)
我尝试通过显式声明类型为
TEMPLATE_MAP
的类型来修复它,目的是将对象的键的类型声明为字符串而不是文字。
// Added the type to `TEMPLATE_MAP`
const TEMPLATE_MAP: Record<string, ReturnType<typeof createTemplate>> = {
One: FirstTemplate,
Two: SecondTemplate,
}
但是现在我在尝试声明数组元素的行上遇到了输入错误。错误看起来像
Type '{ pick: (items: Items) => { foo: string; bar: number; }; use: (props: { foo: string; bar: number; }) => string; }' is not assignable to type '{ pick: (items: Items) => Record<string, string | number>; use: (props: Record<string, string | number>) => string; }'.
Types of property 'use' are incompatible.
Type '(props: { foo: string; bar: number; }) => string' is not assignable to type '(props: Record<string, string | number>) => string'.
Types of parameters 'props' and 'props' are incompatible.
Type 'Record<string, string | number>' is missing the following properties from type '{ foo: string; bar: number; }': foo, bar(2322)
我的期望是它会将对象中的值类型(即键为常量且值为特定形状的对象)拓宽为
Record<string, string | number>
——更广泛的类型。
首先,没有原则性的方法可以让
key
成为 string
并让它发挥作用。如果您给TemplateMap
一个字符串索引签名,那么就没有非常有用的类型可供您指定为属性。为了安全起见,你最终会得到 pick
/use
对的 union,并且 函数unions 最终只能通过参数的 intersections 进行调用,以保证安全。
正确的做法是让
TemplateMap
被推断出来:const TemplateMap = {
One: FirstTemplate,
Two: SecondTemplate,
}
然后您希望
key
成为这些键之一,
"One"
或
"Two"
(即
keyof typeof TemplateMap
):const evaluateTemplate = (items: Items, key: "One" | "Two") => {
const template = TemplateMap[key];
const picked = template.pick(items)
const evaluated = template.use(picked); // error!
/* Argument of type
'{ foo: string; bar: number; } | { foo: number; bar: string; }'
is not assignable to parameter of type
'{ foo: string; bar: number; } & { foo: number; bar: string; }'. */
return evaluated
}
但还是不行,原因是一样的。
template
的类型是
pick
/
use
对的并集,一旦你尝试调用它,编译器就会生气。 TypeScript 没有意识到
template.pick
和
template.use
彼此相关。它将它们视为独立的函数联合,因此您最终会发现
template.pick(items)
生成参数联合,而
template.use
是函数联合,并且您无法安全地将前者传递给后者。microsoft/TypeScript#30581中描述了缺乏对同一联合类型值的多个话语之间的相关性的跟踪(在本例中为
template
)。
建议的方法是从联合进行重构,转向“受相关联合约束”的“泛型”。 所有操作都需要用以下形式编写: “基本”键值映射,代表代码抽象的最基本、最简单的类型关系;
interface MyIO {
One: {
foo: string;
bar: number;
};
Two: {
foo: number;
bar: string;
};
}
type TemplateMap = { [K in keyof MyIO]:
{
pick: (items: Items) => MyIO[K],
use: (props: MyIO[K]) => string
}};
const TemplateMap: TemplateMap = {
One: FirstTemplate,
Two: SecondTemplate,
}
const evaluateTemplate = <K extends keyof TemplateMap>(items: Items, key: K) => {
const template = TemplateMap[key];
const picked = template.pick(items)
const evaluated = template.use(picked) // okay
return evaluated
}
这里的“基础”键值映射是
MyIO
,它显示了
One
/
Two
与pick
返回并由use
消耗的类型之间的关系。那么 TemplateMap
是 MyIO
上的映射类型,并且 TemplateMap
值被 注释为具有
TemplateMap
类型。 最后 evaluateTemplate
是 K
的 key
类型上的 generic 函数。现在 template
的类型为 TemplateMap[K]
。因此,TypeScript 将 picked
视为 MyIO[K]
类型,将 template.use
视为 (props: MyIO[K]) => string
类型。 因此,我们没有参数的联合和函数的联合,而是有一个“泛型”参数和一个采用相同泛型参数的函数。 通话成功。
这主要是你问题的答案,只不过这个公式让你分别指定
MyIO
的类型和 TemplateMap
的内容,这是多余的。最好从 MyIO
变量
TemplateMap
计算。 只要我们将
TemplateMap
变量重命名,然后稍后再赋值即可完成此操作:
const _TemplateMap = {
One: FirstTemplate,
Two: SecondTemplate,
}
type _TM = typeof _TemplateMap;
type MyIO = { [K in keyof _TM]: ReturnType<_TM[K]["pick"]> }
const TemplateMap: TemplateMap = _TemplateMap;
这里我将原始值命名为_TemplateMap
。然后我从 MyIO
计算
typeof _TemplateMap
;您可以验证它是否与以前相同。然后我们声明
TemplateMap
,将其注释为 TemplateMap
,并将 _TemplateMap
赋值给它。 之后的一切都保持不变。所以现在你可以向
_TemplateMap
添加新成员,只要他们满足与其他人相同的总契约,你的代码就会继续工作。
Playground 代码链接