Is it possible to stay DRY when defining a complex index signature in TypeScript

huangapple go评论119阅读模式
英文:

Is it possible to stay DRY when defining a complex index signature in TypeScript

问题

export interface Type {
name: string;
extraField: number;
}

const keyValueMap: { [name: string]: Type } = {
keyOne: {
name: 'keyOne',
extraField: 1,
},
keyTwo: {
name: 'keyTwo',
extraField: 2,
},
};

如果我想要使用索引签名定义键值映射,有没有办法保持DRY(不重复自己)并且在某种程度上避免复制键名?

英文:
export interface Type {
  name: string;
  extraField: number;
}

const keyValueMap: { [name: string]: Type } = {
  keyOne: {
    name: 'keyOne',
    extraField: 1,
  },
  keyTwo: {
    name: 'keyTwo',
    extraField: 2,
  },
};

Assuming I would want to define a key-value map using an index signature is there a way to stay DRY and to somehow avoid copying the key name?

答案1

得分: 1

你可以创建一个辅助函数,该函数将接受一个包含这些对象的数组,并返回一个对象 — 该函数将每个输入对象分配给一个与其name属性的值相对应的属性:

TS Playground

type Mutable<T> = { -readonly [K in keyof T]: T[K] };

type NameMap<U extends { readonly name: string }> =
  { [T in U["name"]]: Mutable<T> };

type Item<
  Name extends string = string,
  Num extends number = number,
> = {
  name: Name;
  extraField: Num;
};

function createNameMap<Items extends ReadonlyArray<Readonly<Item>>>(
  items: Items,
): NameMap<Items[number]> {
  const map: any = {};
  for (const item of items) map[item["name"]] = item;
  return map;
}

const actual = createNameMap([
  { name: "keyOne", extraField: 1 },
  { name: "keyTwo", extraField: 2 },
] as const);
//^^^^^^^^
// Use a const assertion to infer literal property values

actual.keyOne.name
            //^? (property) name: "keyOne"

actual.keyOne.extraField
            //^? (property) extraField: 1

actual.keyTwo.name
            //^? (property) name: "keyTwo"

actual.keyTwo.extraField
            //^? (property) extraField: 2

const expected = {
  keyOne: { name: "keyOne", extraField: 1 },
  keyTwo: { name: "keyTwo", extraField: 2 },
};

const equalAtRuntime = JSON.stringify(actual) === JSON.stringify(expected);

console.log("Equal:", equalAtRuntime ? "✅" : "❌"); //=> Equal: ✅

Compiled JS from the TS playground:

"use strict";
function createNameMap(items) {
    const map = {};
    for (const item of items)
        map[item["name"]] = item;
    return map;
}
const actual = createNameMap([
    { name: "keyOne", extraField: 1 },
    { name: "keyTwo", extraField: 2 },
]);
//^^^^^^^^
// Use a const assertion to infer literal property values
actual.keyOne.name;
//^?
actual.keyOne.extraField;
//^?
actual.keyTwo.name;
//^?
actual.keyTwo.extraField;
//^?
const expected = {
    keyOne: { name: "keyOne", extraField: 1 },
    keyTwo: { name: "keyTwo", extraField: 2 },
};
const equalAtRuntime = JSON.stringify(actual) === JSON.stringify(expected);
console.log("Equal:", equalAtRuntime ? "✅" : "❌"); //=> Equal: ✅
英文:

You can create a helper function that will accept an array of such objects and return an object — the function will assign each input object to a property which corresponds to the value of its name property:

TS Playground

type Mutable&lt;T&gt; = { -readonly [K in keyof T]: T[K] };

type NameMap&lt;U extends { readonly name: string }&gt; =
  { [T in U as T[&quot;name&quot;]]: Mutable&lt;T&gt; };

type Item&lt;
  Name extends string = string,
  Num extends number = number,
&gt; = {
  name: Name;
  extraField: Num;
};

function createNameMap&lt;Items extends ReadonlyArray&lt;Readonly&lt;Item&gt;&gt;&gt;(
  items: Items,
): NameMap&lt;Items[number]&gt; {
  const map: any = {};
  for (const item of items) map[item[&quot;name&quot;]] = item;
  return map;
}

const actual = createNameMap([
  { name: &quot;keyOne&quot;, extraField: 1 },
  { name: &quot;keyTwo&quot;, extraField: 2 },
] as const);
//^^^^^^^^
// Use a const assertion to infer literal property values

actual.keyOne.name
            //^? (property) name: &quot;keyOne&quot;

actual.keyOne.extraField
            //^? (property) extraField: 1

actual.keyTwo.name
            //^? (property) name: &quot;keyTwo&quot;

actual.keyTwo.extraField
            //^? (property) extraField: 2

const expected = {
  keyOne: { name: &quot;keyOne&quot;, extraField: 1 },
  keyTwo: { name: &quot;keyTwo&quot;, extraField: 2 },
};

const equalAtRuntime = JSON.stringify(actual) === JSON.stringify(expected);

console.log(&quot;Equal:&quot;, equalAtRuntime ? &quot;✅&quot; : &quot;❌&quot;); //=&gt; Equal: ✅

Compiled JS from the TS playground:

<!-- begin snippet: js hide: true console: true babel: false -->

<!-- language: lang-js -->

&quot;use strict&quot;;
function createNameMap(items) {
    const map = {};
    for (const item of items)
        map[item[&quot;name&quot;]] = item;
    return map;
}
const actual = createNameMap([
    { name: &quot;keyOne&quot;, extraField: 1 },
    { name: &quot;keyTwo&quot;, extraField: 2 },
]);
//^^^^^^^^
// Use a const assertion to infer literal property values
actual.keyOne.name;
//^?
actual.keyOne.extraField;
//^?
actual.keyTwo.name;
//^?
actual.keyTwo.extraField;
//^?
const expected = {
    keyOne: { name: &quot;keyOne&quot;, extraField: 1 },
    keyTwo: { name: &quot;keyTwo&quot;, extraField: 2 },
};
const equalAtRuntime = JSON.stringify(actual) === JSON.stringify(expected);
console.log(&quot;Equal:&quot;, equalAtRuntime ? &quot;✅&quot; : &quot;❌&quot;); //=&gt; Equal: ✅

<!-- end snippet -->

答案2

得分: -1

如果你强烈要求保留name属性,你可以利用泛型。您可以使用下面的片段,它将限制您只能添加与映射属性键相同的键,并为属性名称启用智能感知。

type properties = "keyOne" | "keyTwo"

export interface Type<propertyKey> {
  name: propertyKey;
  extraField: number;
}

type data = {
  [propertyKey in properties]: Type<propertyKey>;
};

const keyValueMap: data = {
  keyOne: {
    name: 'keyOne',
    extraField: 1,
  },
  keyTwo: {
    name: 'keyTwo',
    extraField: 2,
  },
};
英文:

If you ask to stay DRY than, I don't think its a good Idea to duplicate map key name in the child property, it's inevitable that you will access the object with the parent key name so you already have it there. You can discard the name property from the child.

Somehow if you insist on keeping the name property you can leverage Generics. You can use the snippet below, it will restrict you to only add same key as of map property key and enables IntelliSense for the property name.

<!-- begin snippet: js hide: false console: true babel: true -->

<!-- language: lang-js -->

type properties = &quot;keyOne&quot; | &quot;keyTwo&quot;

export interface Type&lt;propertyKey&gt; {
  name: propertyKey;
  extraField: number;
}

type data = {
  [propertyKey in properties]: Type&lt;propertyKey&gt;;
};

const keyValueMap: data = {
  keyOne: {
    name: &#39;keyOne&#39;,
    extraField: 1,
  },
  keyTwo: {
    name: &#39;keyTwo&#39;,
    extraField: 2,
  },
};

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年3月12日 15:54:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/75711753.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定