创建集合时的正确输入,其中元素由同一通用函数的不同实例组成

问题描述 投票:0回答:1

我想创建一个对象,其中的值都是同一泛型的不同实例。然后,我想动态地键入对象以获取和使用该值,所有这些都以类型安全的方式进行。

该对象旨在包含多个 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
}

TS游乐场

不幸的是,打字稿 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,
}

TS游乐场

但是现在我在尝试声明数组元素的行上遇到了输入错误。错误看起来像

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>
——更广泛的类型。

typescript typescript-generics typescript-types
1个回答
0
投票

首先,没有原则性的方法可以让

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 代码链接

    

© www.soinside.com 2019 - 2024. All rights reserved.