#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
литерального значения свойства этого индекса. Затем мы присваиваем этот ключ значению этого индекса в исходном массиве.