使用动态键在 Typescript 中动态创建对象,无需将类型扩展为 { [key: string]: T }
Dynamically create objects in Typescript using dynamic keys, without widening type to { [key: string]: T }
没有扩展到 { [key: string]: V }
的动态对象键?
我正在尝试创建一个 Typescript 函数来 生成一个带有动态键的对象,其名称在函数签名中提供,而 return 类型不会扩大到 { [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';
在彩虹对象上创建两个 dynamic properties:
const rainbow = {
[color1]: { description: 'First color in rainbow' },
[color2]: { description: 'Second color in rainbow' }
};
现在让我们看看我们创建的类型:
type rainbowType = typeof rainbow;
编译器足够聪明,知道第一个 属性 必须命名为 'red'
,第二个称为 'orange'
,给出以下 type。这使我们能够对任何类型为 typeof rainbow
:
的对象使用自动完成
type rainbowType = {
red: {
description: string;
};
orange: {
description: string;
};
}
感谢打字稿!
所以现在我们已经确认我们可以创建一个具有动态属性的对象,并且它不会总是最终被键入为{ [key: string]: V }
。当然把它放到一个方法中会使事情复杂化...
第一次尝试
所以现在第一次尝试创建我们的方法,看看我们是否可以欺骗编译器给出正确的结果。
function createObject<K extends 'item' | 'type' | 'template', T>(keyName: K, details: T)
{
return {
[keyName]: details
};
}
这是一个函数,我将为其提供一个动态键名称(限制为三个选项之一)并为其分配一个值。
const templateObject = createObject('template', { something: true });
这将 return 一个运行时 值 为 { template: { something: true } }
的对象。
不幸的是,这个类型已经扩大了,你可能担心:
typeof templateObject = {
[x: string]: {
something: boolean;
};
}
使用条件类型的解决方案
现在 是解决此问题的简单方法,如果您只有几个可能的键名:
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;
}
这使用 conditional type 强制 return 类型具有与传入的值相同的明确命名的键。
如果我调用 createObject('item', { color: 'red' })
,我将返回一个 type 的对象:
{
item: {
color: string;
};
}
此策略要求您在需要新键名时更新方法,但这并不总是有效。
这通常是讨论结束的地方!
新的 Typescript 功能怎么样?
我对此并不满意,想做得更好。所以我想知道 Template literals 等较新的 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;
不幸的是,您实际上只是在创建一个 'smarter string type'。
我非常努力地尝试,但无法进一步使用模板文字。
那Key Remapping via as
呢...
我能否重新映射一个键并让编译器在 returned 类型中保留新的键名......
事实证明它有效,但有点糟糕:
键重映射仅适用于映射类型,而我没有映射类型。但也许我可以做一个。使用只有一个键的虚拟类型,然后映射它怎么样?
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;
}
这种类型 可以 从函数中 return 编辑,而不是转换成愚蠢的字符串索引对象。
认识 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 });
如果我想使用字符串怎么办?
事实证明,我们可以更进一步,将键 type/name 更改为纯字符串,一切仍然有效!
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' ] }) };
并且typeof dynamicObject
变成了
{
animals: {
animalTable: string[];
};
colors: {
colorTable: string[];
};
}
所以有时 Typescript 比它想象的要聪明。
我的实际问题是什么?这是我们能做到的最好的吗?我应该提出建议问题吗?我错过了什么吗?你学到什么了吗?我是第一个这样做的人吗?
你可以让 mapped types iterate over any key or union of keys you want without having to resort to remapping-with-as
. Mapped types don't need to look like [P in keyof T]
... they can be [P in K]
for any keylike K
. The canonical example of this is the Record<K, T>
utility type whose definition 看起来像这样:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
表示一个对象类型,其键类型为K
,值类型为T
。这基本上就是您的 StrongKey<K, T>
在不通过 keyof { dynamicKey: any }
.
的虚拟中间人的情况下所做的事情
所以你的 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[];
};
} */
没有扩展到 { [key: string]: V }
的动态对象键?
我正在尝试创建一个 Typescript 函数来 生成一个带有动态键的对象,其名称在函数签名中提供,而 return 类型不会扩大到 { [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';
在彩虹对象上创建两个 dynamic properties:
const rainbow = {
[color1]: { description: 'First color in rainbow' },
[color2]: { description: 'Second color in rainbow' }
};
现在让我们看看我们创建的类型:
type rainbowType = typeof rainbow;
编译器足够聪明,知道第一个 属性 必须命名为 'red'
,第二个称为 'orange'
,给出以下 type。这使我们能够对任何类型为 typeof rainbow
:
type rainbowType = {
red: {
description: string;
};
orange: {
description: string;
};
}
感谢打字稿!
所以现在我们已经确认我们可以创建一个具有动态属性的对象,并且它不会总是最终被键入为{ [key: string]: V }
。当然把它放到一个方法中会使事情复杂化...
第一次尝试
所以现在第一次尝试创建我们的方法,看看我们是否可以欺骗编译器给出正确的结果。
function createObject<K extends 'item' | 'type' | 'template', T>(keyName: K, details: T)
{
return {
[keyName]: details
};
}
这是一个函数,我将为其提供一个动态键名称(限制为三个选项之一)并为其分配一个值。
const templateObject = createObject('template', { something: true });
这将 return 一个运行时 值 为 { template: { something: true } }
的对象。
不幸的是,这个类型已经扩大了,你可能担心:
typeof templateObject = {
[x: string]: {
something: boolean;
};
}
使用条件类型的解决方案
现在 是解决此问题的简单方法,如果您只有几个可能的键名:
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;
}
这使用 conditional type 强制 return 类型具有与传入的值相同的明确命名的键。
如果我调用 createObject('item', { color: 'red' })
,我将返回一个 type 的对象:
{
item: {
color: string;
};
}
此策略要求您在需要新键名时更新方法,但这并不总是有效。
这通常是讨论结束的地方!
新的 Typescript 功能怎么样?
我对此并不满意,想做得更好。所以我想知道 Template literals 等较新的 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;
不幸的是,您实际上只是在创建一个 'smarter string type'。
我非常努力地尝试,但无法进一步使用模板文字。
那Key Remapping via as
呢...
我能否重新映射一个键并让编译器在 returned 类型中保留新的键名......
事实证明它有效,但有点糟糕:
键重映射仅适用于映射类型,而我没有映射类型。但也许我可以做一个。使用只有一个键的虚拟类型,然后映射它怎么样?
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;
}
这种类型 可以 从函数中 return 编辑,而不是转换成愚蠢的字符串索引对象。
认识 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 });
如果我想使用字符串怎么办?
事实证明,我们可以更进一步,将键 type/name 更改为纯字符串,一切仍然有效!
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' ] }) };
并且typeof dynamicObject
变成了
{
animals: {
animalTable: string[];
};
colors: {
colorTable: string[];
};
}
所以有时 Typescript 比它想象的要聪明。
我的实际问题是什么?这是我们能做到的最好的吗?我应该提出建议问题吗?我错过了什么吗?你学到什么了吗?我是第一个这样做的人吗?
你可以让 mapped types iterate over any key or union of keys you want without having to resort to remapping-with-as
. Mapped types don't need to look like [P in keyof T]
... they can be [P in K]
for any keylike K
. The canonical example of this is the Record<K, T>
utility type whose definition 看起来像这样:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
表示一个对象类型,其键类型为K
,值类型为T
。这基本上就是您的 StrongKey<K, T>
在不通过 keyof { dynamicKey: any }
.
所以你的 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[];
};
} */