TypeScript:将对象条目映射到类型

Van*_* D. 5 generics type-inference typescript typescript-generics

作为打字稿新手用户,我什至在提出问题时都遇到困难,所以请耐心等待。

我试图创建一个 key => [string + valueObject 接口] 字符串和 valueObjects (作为类型)的映射,然后有一个函数,它根据传递的键强制执行 valueObject 接口。

我觉得最好用一个例子来解释:

// This is an pseudo example stub, not actually working

type ReplaceableWith<T> = string;
//                   ^ the type I'd like to enforce as the argument

const templates = {
  // templateId    // template               // define somehow the interface required for this template
  'animal.sound': 'A {animal} goes {sound}' as ReplaceableWith<{ animal: string; sound: string}>
};

function renderTemplate(
  templateId , // must be a key of templates
  params // must match value object type, based on templateId
): string {
  let rendered = templates[templateId];
  for (const [key, value] of Object.entries(params)) {
    // replace keys from template with values
    rendered = rendered.replace('{' + key + '}', value);
  }
  return rendered;
}

const a = renderTemplate('animal.sound', { animal: 'Dog', sound: 'woof' })
//    ^ a = 'A Dog goes woof'
const b = renderTemplate('animal.sound', { name: 'Some' });
//                                         ^ should throw TS error
Run Code Online (Sandbox Code Playgroud)

显然,这个例子不起作用,但我认为它展示了我想要实现的目标。我对 keyof、泛型和枚举进行了一些未经教育的尝试,但没有成功。

这种类型映射(或查找)是否可能?


更新(工作示例)

经过一番尝试后,这是一个包含潜在解决方案的工作示例:

type TemplateKeys = {
  'animal.sound': { animal: string; sound: string };
  'animal.sleep': { location: string };
  'animal.herd': { expectedCount: number; available: number };
  'animal.think': undefined;
};

const templates: {[key in keyof TemplateKeys]: string} = {
  'animal.sound': '{animal} goes {sound}',
  'animal.sleep': 'It sleeps in {location}',
  'animal.herd': 'There is {available} animals out of {expectedCount}',
  'animal.think': 'Its thinking'
};

function renderTemplate<K extends keyof TemplateKeys>(key: K, params?: TemplateKeys[K]): string {
  if (params !== undefined) {
    //@ts-ignore
    return Object.entries(params).reduce((previousValue: string, [param, value]: [string, any]) => {
      return previousValue.replace('{' + param + '}', value);
    }, templates[key]);
  }
  return templates[key];
}

console.log(renderTemplate('animal.sound', { animal: 'Dog', sound: 'woof' }));
console.log(renderTemplate('animal.sleep', { location: 'a hut' }));
console.log(renderTemplate('animal.herd', { expectedCount: 20, available: 10 }));
console.log(renderTemplate('animal.think'));
Run Code Online (Sandbox Code Playgroud)

输出:

[LOG]: Dog goes woof 
[LOG]: It sleeps in a hut 
[LOG]: There is 10 animals out of 20 
[LOG]: Its thinking 
Run Code Online (Sandbox Code Playgroud)

虽然这有效,但它有两个问题:

  1. 我必须定义键两次(在接口和实例中)。
  2. 参数接口和消息是分开的,理想情况下它们应该在一起。

cap*_*ian 1

2021 年 9 月 17 日更新

我已经删除了所有重复的代码。

我还创建了允许参数的联合 - Params。请参阅更新的示例

const enum Animal {
  sound = 'animal.sound',
  sleep = 'animal.sleep',
  herd = 'animal.herd',
  think = 'animal.think',
}

type TemplateKeys = {
  [Animal.sound]: { animal: string; sound: string };
  [Animal.sleep]: { location: string };
  [Animal.herd]: { expectedCount: number; available: number };
  [Animal.think]?: string;
};

const templates = {
  [Animal.sound]: '{animal} goes {sound}',
  [Animal.sleep]: 'It sleeps in {location}',
  [Animal.herd]: 'There is {available} animals out of {expectedCount}',
  [Animal.think]: 'Its thinking'
} as const;

type Templates = typeof templates;

type Values<T> = T[keyof T]

type Dictionary = {
  [Prop in keyof TemplateKeys]:
  (key: Prop, params: TemplateKeys[Prop]) => string
}

type Params =
  Parameters<
    NonNullable<
      Values<Dictionary>
    >
  >

const renderTemplate = (...params: Params) => {
  const [key, ...props] = params;
  if (props !== undefined) {
    // @ts-ignore
    const result = (Object.entries(props) as Array<Params>)
      .reduce<string>((previousValue, [param, value]) =>
        typeof value === 'string'
          ? previousValue.replace('{' + param + '}', value)
          : previousValue,
        templates[key]
      );

    return result


  }
  return templates[key];
}


renderTemplate(Animal.sleep, { location: 'a hut' }) // ok

renderTemplate(Animal.herd, { expectedCount: 20, available: 10 }) // ok
renderTemplate(Animal.sound, { location: 'sd' }) // expected error
Run Code Online (Sandbox Code Playgroud)

我已经Animal为键创建了枚举。别担心,枚举不会包含在您的捆绑包中。

这是payground的链接 这是Object.entries 类型 的链接

解释:

Dictionary- 主要实用类型。它创建一个带有Animal键和函数作为值的对象。该函数已经考虑了特定键允许的道具。

例子:

type Dictionary = {
    "animal.sound": (key: Animal.sound, params: {
        animal: string;
        sound: string;
    }) => string;
    "animal.sleep": (key: Animal.sleep, params: {
        location: string;
    }) => string;
    "animal.herd": (key: Animal.herd, params: {
        expectedCount: number;
        available: number;
    }) => string;
    "animal.think"?: ((key: Animal.think, params: string | undefined) => string) | undefined;
}
Run Code Online (Sandbox Code Playgroud)

现在,我们需要将此数据结构转换为具有两个元素的元组。第一个元素 - 一个键,第二个元素 - 是允许的参数。

你可能会问:为什么我们需要一个元组?因为rest参数是一个元组。

因此,我们只需要在实用程序的帮助下获取所有值,并在内置实用程序Values的帮助下获取它们的参数:Parameters

type Params =
  | [Animal.sound, {
    animal: string;
    sound: string;
  }]
  | [Animal.sleep, {
    location: string;
  }]
  | [Animal.herd, {
    expectedCount: number;
    available: number;
  }]
  | [Animal.think, string | undefined]

type Params =
  Parameters<
    NonNullable<
      Values<Dictionary>
    >
  >
Run Code Online (Sandbox Code Playgroud)