Я пытаюсь лучше понять, как TypeScript обрабатывает типы для массивов, особенно дженерики во время распространения. Одним из первых вопросов для меня был этот пример:
const spread = <T extends unknown[]>(arr: T): T => [...arr]; //T[number][]' is not assignable to type 'T'
Сначала хотелось бы понять, что мешает T[number][] свести к T. Я более или менее понимаю, что тип unknown[], вероятно, в некотором роде слишком свободный, потому что я могу делать вещи, которые я бы счел неожиданными, например:
const test = <T extends unknown[]>(arr:T):T[number]=>arr[0] //no error
const test2 = <T extends unknown[]>(arr:T):T[number]=>[...arr] //no error
Тип T[number] может принимать много вещей, чего именно в ограничении типа не хватает, и есть ли способ улучшить его для сценария, где я хотел бы сказать, что это просто массив чего-либо?
Попытка этих двух вариантов работает, но для назначения объекта я не понимаю, почему, а для кортежа я не знаю, наиболее ли это полезно.
const tupleSpread = <T extends readonly [unknown,...unknown[]]>(arr: T): T => [...arr]; //no error
const spreadAssign = <T extends unknown[]>(arr: T): T => Object.assign(arr); //no error
Вышеизложенное является основной проблемой моего вопроса. Ниже я приведу небольшой контекст, который представляет собой минимальную, но репрезентативную версию того, что я пытался сделать, что привело меня к вопросу о распространении массива. Вот набросок:
//two possible kinds of value with key for use as discriminant
type DataMap = {
string: string;
number: number;
};
//arrays for these value types mapped over these value types
type ArrayMap = { [K in keyof DataMap]: DataMap[K][] };
//function to apply one payload of type DataMap[T] to one array of type DataMap[K][]
const apply = <T extends keyof DataMap>(
target: ArrayMap[T],
payload: DataMap[T]
): ArrayMap[T] => {
const next = [...target, payload]; //(DataMap[T] | ArrayMap[T][number])[]
return next //not assignable
};
Функция apply не работает с типом, я думаю, что это похоже на пример с простым спредом, только типы немного сложнее. Но по сути я думаю, что ArrayMap[T][number] не сводится к DataMap[T] так же, как T[number][] не сводится к T в первом примере со спредом.
Я попытался немного сдвинуть дженерики, чтобы посмотреть, смогу ли я обойти проблему следующим образом:
const applyByValueType = <T extends DataMap[keyof DataMap]>(
target: T[],
payload: T
): T[] => {
return [...target, payload];
};
Но я снова думаю, что это просто отодвинуло проблему на второй план. Если я попытаюсь использовать эту измененную функцию в контексте, снова появится похожая ошибка:
const unitArrays:ArrayMap = {
number:[],
string:[]
}
const applyAny = <T extends keyof DataMap> (discriminant:T,unit:DataMap[T])=>{
const target = unitArrays[discriminant];
const next = applyByValueType(target, unit)
unitArrays[discriminant] = next; //not assignable
}
Пытаясь понять немного лучше, я создал тип, который будет кортежем, который принимает исходный массив и соответствующую ему полезную нагрузку, называемую приемлемым кортежем.
type AcceptableTuple = { [K in keyof DataMap]: [ DataMap[K][], DataMap[K]] };
И, пытаясь использовать его, я вижу, что члены кортежа могут быть назначены индивидуально, но как только я их соберу, новый кортеж не будет. Кроме того, назначение объекта, похоже, работает и здесь.
const applyAnyAcceptable = <T extends keyof DataMap> (discriminant:T,unit:DataMap[T])=>{
const target = unitArrays[discriminant];
const isTargetAcceptable:AcceptableTuple[T][0]=target; //yes
const isUnitAcceptable:AcceptableTuple[T][1]=unit; //yes
const areBothAcceptable:[AcceptableTuple[T][0],AcceptableTuple[T][0]]=[target,unit] //no
const areBothAccetpable2:AcceptableTuple[T] = [target,unit] as const //no
const areBothAccetpableObject:AcceptableTuple[T] = Object.assign([target,unit]) //works with object assign
}
Вот ссылка на площадку с этими типами.






applyTypescript не выводит отношения между ArrayMap[T] и DataMap[T] в той мере, в какой он может знать, что DataMap[T] всегда можно присвоить ArrayMap[T][number].
T extends keyof DataMap может быть "string" | "number", и, таким образом, DataMap[T] есть string | number.
string | number на самом деле не может быть назначен ни одному элементу в ArrayMap[T], потому что ArrayMap["string"] может содержать только строки, а ArrayMap["number"] может содержать только числа.
К счастью, есть очень простой и прагматичный подход к решению этой проблемы — просто подтвердите, что ваше конкатенированное значение имеет правильный тип, и покончите с этим:
//function to apply one payload of type DataMap[T] to one array of type DataMap[K][]
const apply = <T extends keyof DataMap>(
target: ArrayMap[T],
payload: DataMap[T]
): ArrayMap[T] => {
return [...target, payload] as ArrayMap[T];
};
Кажется, это затрагивает аспекты этого предыдущего ответа, основной вывод из которых заключается в следующем:
[
T extends unknown[]] ... означает, чтоTдолжен быть присваиваемым типу массива, но ему разрешено иметь дополнительные свойства (как того требует структурная типизация). Это не проблема для ввода функции, но не может быть гарантировано выводом.
В качестве конкретного примера рассмотрим распространение массива с дополнительным свойством:
const val = Object.assign([1,2,3], { foo: "bar" });
// typeof val is (number[] & { foo: string })
const spreadVal = [...val];
// typeof spreadVal is (number[])
Таким образом, ваш исходный spread не выполняет свой контракт — оператор спреда может отбросить свойства, поэтому [...val] не имеет рекламируемого возвращаемого типа T.
Есть несколько способов, которыми вы можете пойти, в зависимости от того, что вы хотите сделать.
Вы можете изменить свой параметр типа, чтобы он был типом элемента массива, а не типом всего массива:
const spread = <T>(arr: T[]): T[] => [...arr];
TS делает вывод T на основе всех входных элементов, расширяя их соответствующим образом, поэтому spread([1,2,3]) вернет number[], а spread([1,2,"abc"]) вернет (string|number)[].
Если вы хотите сохранить T в качестве типа самого входного массива, вы можете добиться того же результата, используя расширение кортежа TS для определения возвращаемого типа:
const spread = <T extends unknown[]>(arr: T): [...T] => [...arr];
Если вы надеетесь сохранить немного большую точность в отношении типа ввода и используете TS5, вы можете использовать немного громоздкий параметр типа const:
const spread = function<const T extends readonly unknown[]>(arr: T)
: readonly [...T] { return [...arr]; }
Это позволит сохранить тип возвращаемого значения несколько более узким, чем в приведенных выше подходах, поэтому spread([1,2,"abc"]) будет возвращать тип [1,2,"abc"].
Также стоит упомянуть, что вы можете заставить ваше исходное объявление работать, используя оператор распространения объекта, а не оператор распространения массива:
const spread = <T extends unknown[]>(arr: T): T => ({...arr});
Это выглядит немного странно, но поскольку вы обрабатываете входной массив как объект, копируются все его свойства, а не только записи с числовым индексом и .length.
Возвращаясь к вашим исходным примерам, мы понимаем, почему некоторые из ваших попыток работают:
// SUCCEEDS because T[number] only includes the numerically-indexed
// properties copied across by the spread operator
const test2 = <T extends unknown[]>(arr:T):T[number]=>[...arr]
// SUCCEEDS because Object.assign copies all properties across from
// the input - same as ({ ...arr })
const spreadAssign = <T extends unknown[]>(arr: T): T => Object.assign(arr);
const spread = <T extends any>(arr: T[]): T[] => [...arr]должно работать.