Динамически создавать объекты в Typescript с помощью динамических ключей без расширения типа до {[key: string]: T}

Ключ динамического объекта без расширения до { [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 умнее, чем кажется.

Какой у меня настоящий вопрос? Это лучшее, что мы когда-либо сможем сделать? Должен ли я сообщить о проблеме с предложением? Я что-нибудь пропустил? Вы чему-нибудь научились? Я первый, кто это сделал?


person Simon_Weaver    schedule 23.06.2021    source источник
comment
Йо, это вопрос. Мне пришлось прокрутить вниз, чтобы посмотреть, закончится ли это. Хорошо, прокрутите назад   -  person smac89    schedule 23.06.2021
comment
@ smac89 Я слишком поздно задержал меня прошлой ночью - хотел, чтобы все это было записано для ввода. Не смог найти другого решения этой проблемы, но нашел много похожих вопросов.   -  person Simon_Weaver    schedule 23.06.2021


Ответы (1)


Вы можете заставить сопоставленные типы перебирать любой ключ или объединение ключи, которые вы хотите, не прибегая к повторному сопоставлению-с-as. Сопоставленные типы не обязательно должны выглядеть _2 _... они могут быть [P in K] для любого типа ключа K. Каноническим примером этого является тип утилиты Record<K, T>, выглядит следующим образом

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[];
    };
} */

площадка ссылка на код

person jcalz    schedule 23.06.2021