使用动态键在 Typescript 中动态创建对象,无需将类型扩展为 { [key: string]: T }

2023-11-22

动态对象键,无需扩展至{ [key: string]: V } ?

我正在尝试创建一个 Typescript 函数来生成一个具有动态键的对象,其名称在函数签名中提供,而返回类型不会扩展为{ [key: string]: V }.

所以我想打电话:

createObject('template', { items: [1, 2, 3] })

并取回一个对象{ template: { items: [1, 2, 3] } }这不仅仅是一个带有字符串键的对象。

快速绕道——让我们创造彩虹!

在你告诉我这是不可能之前,让我们先制作一条彩虹:

type Color = 'red' | 'orange' | 'yellow';
 
const color1: Color = 'red';
const color2: Color = 'orange';    

创建两个动态特性在彩虹物体上:

const rainbow = {
    [color1]: { description: 'First color in rainbow' },
    [color2]: { description: 'Second color in rainbow' }
};

现在让我们看看type我们创造的:

 type rainbowType = typeof rainbow;

编译器足够聪明,知道第一个属性必须命名'red'第二个叫做'orange',给出以下type。这使我们能够对任何键入为的对象使用自动完成功能typeof rainbow:

type rainbowType = {
    red: {
        description: string;
    };
    orange: {
        description: string;
    };
}

谢谢打字稿!

现在我们已经确认我们可以创建一个具有动态属性的对象,但它不会always最终输入为{ [key: string]: V }。当然,将其放入方法中会使事情变得复杂......

第一次尝试

现在我们第一次尝试创建我们的方法,看看我们是否可以欺骗编译器给出正确的结果。

function createObject<K extends 'item' | 'type' | 'template', T>(keyName: K, details: T)
{
    return {
        [keyName]: details
    };
}

这是一个我将提供的函数动态键名(仅限于三个选择之一)并为其分配一个值。

const templateObject = createObject('template', { something: true });

这将返回一个具有运行时的对象value of { template: { something: true } }.

不幸的是,正如您可能担心的那样,这种类型已经扩大了:

typeof templateObject = {
    [x: string]: {
        something: boolean;
    };
}

使用条件类型的解决方案

现在那里is如果您只有几个可能的键名称,则有一个简单的方法可以解决此问题:

function createObject<K extends 'item' | 'type' | 'template', T extends {}>(keyName: K, details: T)
{
    const newObject: K extends 'item' ? { item: T } :
                     K extends 'type' ? { type: T } :
                     K extends 'template' ? { template: T } : never = <any>{ [keyName]: details };

    return newObject;
}

这使用了一个条件类型强制返回类型具有与传入值相同的显式命名键。

如果我打电话createObject('item', { color: 'red' })我会用 a 取回一个对象type of:

{
    item: {
        color: string;
    };
}

如果您需要新的密钥名称,此策略要求您更新方法,但这并不总是有效。

讨论通常就到此结束!

Typescript 的新功能怎么样?

我对此并不满意,还想做得更好。所以我想知道是否有任何较新的 Typescript 语言功能,例如模板文字可能有帮助。

您可以做一些非常聪明的事情,如下所示(这只是一个随机有趣的不相关示例):

type Animal = 'dog' | 'cat' | 'mouse';
type AnimalColor = 'brown' | 'white' | 'black';

function createAnimal<A extends Animal, 
                      C extends AnimalColor>(animal: A, color: C): `${C}-${A}`
{
    return <any> (color + '-' + animal);
}

这里的魔力在于模板文字${C}-${A}。那么让我们创造一个动物......

const brownDog = createAnimal('dog', 'brown');;

// the type brownDogType is actually 'brown-dog' !!
type brownDogType = typeof brownDog;

不幸的是,在这里您实际上只是创建了一个“更智能的字符串类型”。

我非常努力地尝试,但无法进一步使用模板文字。

关于什么按键重新映射通过as...

我可以重新映射一个键并让编译器在返回的类型中保留该新键名称吗......

事实证明它有效,但有点糟糕:

键重新映射仅适用于映射类型而且我没有映射类型。但也许我可以做一个。如何采用只有一个键的虚拟类型并映射它?

type DummyTypeWithOneKey = { _dummyProperty: any };

// this is what we want the key name to be (note: it's a type, not a string)
type KEY_TYPE = 'template';

// Now we use the key remapping feature
type MappedType = { [K in DummyTypeWithOneKey as `${ KEY_TYPE }`]: any };

这里的神奇之处在于as ${ KEY_TYPE }` 更改键的名称。

编译器现在告诉我们:

type MappedType = {
    template: any;
}

这个类型can从函数返回并且不会转换为愚蠢的字符串索引对象。

认识 StrongKey

那么现在怎么办?我可以创建一个辅助类型来抽象出可怕的虚拟类型。

请注意,自动完成建议_dummyProperty所以我把它重命名为dynamicKey.

export type StrongKey<K extends string, V> = { [P in keyof { dynamicKey: any } as `${ K }`]: V };

function createObject<K extends 'item' | 'type' | 'template', T extends {}>(keyName: K, details: T)
{
    return { [keyName]: details } as StrongKey<K, T>;
}

如果我现在执行以下操作,我最终将获得 obj 的自动完成功能!

const obj = createObject('type', { something: true });

如果我想使用字符串怎么办?

事实证明,我们可以更进一步,将密钥类型/名称更改为纯字符串,一切仍然有效!

export type StrongKey<K extends string, V> = { [P in keyof { dynamicKey: any } as `${ K }`]: V };

// creates a 'wrapped' object using a well known key name
function createObject<K extends string, T extends {}>(keyName: K, details: T)
{
    return { [keyName]: details } as StrongKey<K, T>;
}

合并生成的对象

如果我们不能组合生成的对象,那么一切都是徒劳的。幸运的是,以下工作有效:

const dynamicObject = {...createObject('colors', { colorTable: ['red', 'orange', 'yellow' ] }),
                       ...createObject('animals', { animalTable: ['dog', 'cat', 'mouse' ] }) };

And typeof dynamicObject becomes

{
    animals: {
        animalTable: string[];
    };
    colors: {
        colorTable: string[];
    };
}

所以有时 Typescript 比它想象的更聪明。

我真正的问题是什么?这是我们能做的最好的事情吗?我应该提出问题并提出建议吗?我错过了什么吗?你学到什么了吗?我是第一个这样做的人吗?


你(们)能做到映射类型迭代您想要的任何键或键的联合,而不必诉诸重新映射-with-as。映射类型不需要看起来像[P in keyof T]... 他们可以[P in K]对于任何类似钥匙的K。典型的例子是the Record<K, T>实用型 whose 定义看起来像这样:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

并表示一个对象类型,其键为 typeK其值的类型为T。这本质上就是你的StrongKey<K, T>是在不经过虚拟中间人的情况下进行的keyof { dynamicKey: any }.


So your createObject()函数可以写成:

function createObject<K extends string, T extends {}>(keyName: K, details: T) {
  return { [keyName]: details } as Record<K, T>;
}

产生相同的输出:

const dynamicObject = {
  ...createObject('colors', { colorTable: ['red', 'orange', 'yellow'] }),
  ...createObject('animals', { animalTable: ['dog', 'cat', 'mouse'] })
};

/* const dynamicObject: {
    animals: {
        animalTable: string[];
    };
    colors: {
        colorTable: string[];
    };
} */

Playground 代码链接

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用动态键在 Typescript 中动态创建对象,无需将类型扩展为 { [key: string]: T } 的相关文章