Распространение общего массива и типы присвоения

Я пытаюсь лучше понять, как 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
  
}

Вот ссылка на площадку с этими типами.

const spread = <T extends any>(arr: T[]): T[] => [...arr] должно работать.
Roar S. 02.05.2023 20:14
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
1
82
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Краткий ответ на то, что вы пытаетесь сделать с помощью функции apply

Typescript не выводит отношения между 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);

Другие вопросы по теме