В Typescript, как я могу создать универсальный тип keyBy? Создайте объект с ключом определенного столбца из массива объектов

#typescript

Вопрос:

У меня есть массив типов объектов, и я хотел бы преобразовать его в объект, управляемый свойством в каждом из объектов. Концептуально похож на keyBy Lodash, но по типам.

 type Arr = [
    {slug: 'test-1', value: string},
    {slug: 'test-2', value: string},
    {slug: 'test-3', value: string},
]

type KeyBy<A extends object[], S extends keyof A[number]> = {
    //1. how do I get numeric indexes of A for iteration and mapping?
    //2. how do I convert those numeric indexes to the literal types of the matching property?
}

type Keyed = KeyBy<Arr, 'slug'>

/* desired result: 
{
    'test-1': {slug: 'test-1', value: string},
    'test-2': {slug: 'test-2', value: string},
    'test-3': {slug: 'test-3', value: string},
}
*/
 

Я думаю, что общие ограничения, которые я наложил на тип keyBy, подходят, но для этого не требуется.

Ответ №1:

Да, это определенно возможно:

 type KeyedBy<A, S extends PropertyKey> = A extends Array<infer T>
  ? KeyedByInternal<T, S>
  : never;

type KeyedByInternal<T, S extends PropertyKey> = S extends keyof T ? {
  [K in T[S] as Extends<K, PropertyKey>]: Extends<T, Record<S, K>>
} : never;

type Extends<T1, T2> = T1 extends T2 ? T1 : never;
 

Это дает вам все поведение, которое вы хотите, как для селекторов, создающих литеральные типы, так и для селекторов, создающих типы «юниверса» (например string , или number ).:

 // Tests
type Arr = [
    {slug: 'test-1', value: string, id: 1},
    {slug: 'test-2', value: string, id: 2},
    {slug: 'test-3', value: string, id: 3},
]

type Keyed = KeyedBy<Arr, 'slug'>
/* 
{
    "test-3": {
        slug: 'test-3';
        value: string;
        id: 3;
    };
    "test-1": {
        slug: 'test-1';
        value: string;
        id: 1;
    };
    "test-2": {
        slug: 'test-2';
        value: string;
        id: 2;
    };
}
*/


type Keyed2 = KeyedBy<Arr, 'id'>
/* 
{
    3: {
        slug: 'test-3';
        value: string;
        id: 3;
    };
    1: {
        slug: 'test-1';
        value: string;
        id: 1;
    };
    2: {
        slug: 'test-2';
        value: string;
        id: 2;
    };
}
*/


// IT WORKS FOR NON-LITERAL TYPES TOO!
type Keyed3 = KeyedBy<Arr, 'value'>
/*
{
    [x: string]: {
        slug: 'test-1';
        value: string;
        id: 1;
    } | {
        slug: 'test-2';
        value: string;
        id: 2;
    } | {
        slug: 'test-3';
        value: string;
        id: 3;
    };
}
*/
 

Настоящее понимание заключается в том, что мы можем фильтровать правой стороны нашего союза типа С Record<S, K> (где S находится наш литерал типа, представляющий Selector в типе объекта и K текущая ключ Мы вычислений в наш сопоставленный тип) для того, чтобы сосредоточиться значение для филиала(ов) Союзного типа мы имеем дело с, что соответствует нашим селектора.

Комментарии:

1. Ух ты, ладно. Итак, сначала вы используете infer для создания типа объединения элементов массива, затем индексируете объединение значений соответствующих ключей (отфильтровывая любые значения, которые не являются допустимыми ключами свойств), затем присваиваете K исходному типу объединения, но отфильтровываете только для участников, которые расширяют запись, состоящую только из K, и значение под рукой…? Это унизительно, ха. Очень мило, и спасибо вам.

Ответ №2:

Потратив на это немного больше времени, у меня есть рабочее решение для решения этой проблемы с использованием числовых индексов, хотя и не такое надежное, как решение @Sean, поскольку оно не поддерживает универсальные типы, но, возможно, ему легче следовать, по крайней мере, для меня.

 /* helpers */
type IndecesOfArr<Arr extends any[]> = Exclude<Partial<Arr>['length'], Arr['length'] | undefined>;
type Extends<T1, T2> = T1 extends T2 ? T1 : never;

type Arr = [
    {slug: 'test-1', value: string},
    {slug: 'test-1', value: string, test: number}, //matching keys would be unioned
    {slug: 'test-2', value: string},
    {slug: 'test-3', value: string},
    {slug: ['non-indexable'], value: string}, //should be excluded from result
]

type KeyBy<A extends object[], S extends keyof A[number]> = {
    [I in IndecesOfArr<A> as Extends<A[I][S], PropertyKey>]: A[I]
}

type Keyed = KeyBy<Arr, 'slug'>

/* expected result: 
{
    'test-1': {slug: 'test-1', value: string} 
        | { slug: 'test-1', value: string, test: number},
    'test-2': {slug: 'test-2', value: string},
    'test-3': {slug: 'test-3', value: string},
}
*/
 

IndecesOfArr Тип работает из-за обработки машинописного Arr['length'] текста . Когда задан литеральный массив, он фактически сообщает вам длину литерала, а не просто number . Если вы завернете Arr Partial<Arr> его , он сообщит вам все возможные длины (0 — длина) в виде объединения. Мы исключаем число полной длины, чтобы получить объединение всех длин на основе нуля.

В индексе сопоставления мы используем эти индексы и приводим индекс к ключу со значением S литерального значения свойства этого индекса. Затем мы присваиваем этот ключ значению этого индекса в исходном массиве.

Ссылка на игровую площадку