Ключ динамического объекта без расширения до { [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 rainbowType = typeof rainbow;
Компилятор достаточно умен, чтобы знать, что первое свойство должно называться 'red'
, а второе - 'orange'
, что дает следующий тип. Это позволяет нам использовать автозаполнение для любого объекта, набранного как typeof rainbow
:
type rainbowType = {
red: {
description: string;
};
orange: {
description: string;
};
}
Спасибо Typescript!
Итак, теперь мы подтвердили, что можем создать объект с динамическими свойствами, и он не всегда будет набираться как { [key: string]: V }
. Конечно, включение этого в метод усложняет ситуацию ...
Первая попытка
Итак, теперь первая попытка создать наш метод и посмотреть, сможем ли мы обманом заставить компилятор дать правильный результат.
function createObject<K extends 'item' | 'type' | 'template', T>(keyName: K, details: T)
{
return {
[keyName]: details
};
}
Это функция, для которой я предоставлю имя динамического ключа (ограничено одним из трех вариантов) и присвою ему значение.
const templateObject = createObject('template', { something: true });
Это вернет объект со значением времени выполнения, равным { 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;
}
Здесь используется условный тип. чтобы заставить возвращаемый тип иметь явно названный ключ, такой же, как переданное значение.
Если я позвоню createObject('item', { color: 'red' })
, я верну объект с типом:
{
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 };
Магия здесь - _25 _ $ {KEY_TYPE} `, который изменяет имя ключа.
Теперь компилятор сообщает нам, что:
type MappedType = {
template: any;
}
Этот тип можно вернуть из функции и не преобразовать в глупый строковый индексированный объект.
Встречайте StrongKey ‹K, V›
Ну что теперь? Я могу создать вспомогательный тип, чтобы абстрагироваться от ужасного фиктивного типа.
Обратите внимание, что автозаполнение предлагает _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' ] }) };
И typeof dynamicObject
становится
{
animals: {
animalTable: string[];
};
colors: {
colorTable: string[];
};
}
Так что иногда Typescript умнее, чем кажется.
Какой у меня настоящий вопрос? Это лучшее, что мы когда-либо сможем сделать? Должен ли я сообщить о проблеме с предложением? Я что-нибудь пропустил? Вы чему-нибудь научились? Я первый, кто это сделал?