Можно ли заставить TypeScript понять следующее назначение общего объекта?

Рассмотрим приведенный ниже код:

type Type = 'one' | 'two' | 'three'

type TypeSelection = 'one' | 'two'

interface InnerObject<T extends Type> {
  name: string,
  type: T
}

type Obj<K extends Type> = {
  [P in K]?: InnerObject<P>
} & {
  [P in TypeSelection]: InnerObject<P>
}


const obj: Obj<Type> = {
  one: {
    name: 'a',
    type: 'one'
  },
  two: {
    name: 'a',
    type: 'two'
  }
}

function getInnerObject<T extends Type>(key: T) {
  const selectedObj: InnerObject<T> | undefined = obj[key]
  const defaultObj = obj['one']
}

Детская площадка

Мы знаем, что obj[key] вернет InnerObject<T>, поскольку параметр типа T ограничен Types и key имеет тип T.

Несмотря на это, TypeScript выдает ошибку при этом задании: const selectedObj: InnerObject<T> | undefined = obj[key].

Сообщение об ошибке:

Type 'InnerObject<"one"> | InnerObject<"two"> | InnerObject<"three"> | undefined' is not assignable to type 'InnerObject<T> | undefined'.
  Type 'InnerObject<"one">' is not assignable to type 'InnerObject<T>'.
    Type '"one"' is not assignable to type 'T'.
      '"one"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Type'.(2322)

Кажется, он пытается перекрестно присвоить все возможности правой стороны всем возможностям левой стороны.

Есть ли способ дать ему понять, что назначение выполнено без утверждения типа?

@jcalz Спасибо, что присоединились. Рад попробовать, но на самом деле я не понимаю, почему это не работает. Если я удалю & { [P in TypeSelection]: InnerObject<P> } из Obj, ошибка исчезнет, ​​но тогда я перенесу проблему на defaultObj, так как теперь он не знает, что one существует на объекте. Я пытаюсь заставить работать аналогичную, но более сложную функцию из реальной жизни, поэтому ОП был моей первой попыткой MRE. Вопрос в том, как заставить эту функцию работать, но я не знаю, в чем заключается основная проблема/вопрос.

Magnus 03.09.2024 10:02

@jcalz Готово. Надеюсь, это стало немного яснее. Спасибо за руководство.

Magnus 03.09.2024 16:37

Отлично, теперь мы можем рассмотреть проблему. К сожалению, TS не очень хорошо разбирается в общей логике высшего порядка. Можно было бы надеяться, что он сможет распознать, как (X & Y)[K] присваивается X[K] для общего X, но это не так. Лучшее, что вы можете сделать, это расширить, чтобы избавиться от пересечения перед индексацией, как показано в этой ссылке на игровую площадку. Это полностью отвечает на заданный вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?

jcalz 03.09.2024 16:51

@jcalz Отлично. Да, это решает проблему. Меня такое поведение удивляет. Возможно, TS расширяется (X & Y)[K] до (X)[K] | (Y)[K], который нельзя назначить (X)[K]....

Magnus 03.09.2024 17:09

Скорее, он вообще не пытается его расширять или проверять. Многие операции с универсальными типами фактически откладываются. Я напишу ответ, когда у меня будет возможность.

jcalz 03.09.2024 17:11
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
5
73
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Я не вижу хорошей документации по конкретной вещи, которую вы здесь видите (самая близкая, которую я нашел, это ms/TS#56905 ), но в целом TypeScript не знает, как правильно выполнять многие операции с типами общие типы. Часто он либо откладывает вычисление (а затем отказывается рассматривать действительные вещи как возможные для него), либо охотно сводит параметр универсального типа к его ограничению (а затем ошибочно допускает небезопасные события).

В общем, должно быть верно, что (X & Y)[K] можно назначить X[K] (при условии, что K известно как ключ X). И для конкретных типов X, Y и K TypeScript может это проверить, но только потому, что он просто полностью оценивает пересечение и индексированный доступ. Но когда K является параметром универсального типа, TypeScript не может его проверить. Это откладывает оценку и оставляет ее как некую непрозрачную вещь:

const selectedObject = obj[key];
//    ^? const selectedObject: Obj<Type>[T]

Тип Obj<Type> не кодирует напрямую корреляцию между каждым ключом T и типом InnerObject<T>. Это представлено только косвенно через пересечение. И поэтому TypeScript его не видит:

const selectedObject: InnerObject<T> | undefined = obj[key]; // error!

Ошибка — это то, что вы получите, если попытаетесь присвоить obj[key]InnerObject<K> для некоторого общего K, не связанного с key. Единственный способ, при котором присвоение было бы безопасным, - это если бы obj[key] имело тип InnerObject<K> для всех возможных K, и именно здесь, похоже, происходит «перекрестное присвоение».


Если вы хотите, чтобы ваше задание прошло успешно, вам нужно помочь TypeScript увидеть операцию более прямой. Если у вас есть сопоставленный тип 🔁 формы {[P in K]: F<P>}и вы индексируете его с помощью ключа K, то TypeScript увидит его как назначаемый F<K>. По сути, это тип распространяемого объекта, как описано в microsoft/TypeScript#47109. У вас есть необходимый сопоставленный тип как часть определения Obj<K>, поэтому вы сможете безопасно расширить любой Obj<K> до этой части.

Это дает следующий подход:

function getInnerObject<T extends Type>(key: T) {
    const o: { [P in Type]?: InnerObject<P> } = obj; // widen
    const selectedObj: InnerObject<T> | undefined = o[key] // index into widened thing
    const defaultObj = obj['one']
}

Сначала мы расширяем obj от Obj<Type> до o типа {[P in Type]?: InnerObject<P>}. Такое расширение разрешено, поскольку оно идентично одному члену пересечения, а TypeScript допускает расширение от X & Y до X. Затем мы индексируем этот сопоставленный тип с помощью общего key типа T, поэтому TypeScript видит результирующее значение как присваиваемое InnerObject<T> | undefined.

Обратите внимание, что вы все еще сохраняете значение obj для дальнейшего использования. Переменная o существовала только для безопасного выполнения расширения. Вы могли бы сделать это без другой переменной, используя безопасное расширение x satisfies T as T, где satisfies проверяет тип и as расширяет его:

type OT = { [P in Type]?: InnerObject<P> };
const selectedObj: InnerObject<T> | undefined = (obj satisfies OT as OT)[key];

Детская площадка, ссылка на код

Спасибо, @jcalz. Я немного покопался, и это работает, когда InnerObject является примитивным типом. В тот момент, когда он превращается в объект, TS терпит неудачу. См.: tsplay.dev/NDjoVW. Должно быть это ошибка/ошибка/ограничение TS, которую надо исправить, верно? Нет никакой причины, по крайней мере, насколько я могу судить, которая могла бы привести к сбою только потому, что InnerObject является объектом. Что вы думаете? Стоит ли сообщать об ошибке?

Magnus 03.09.2024 23:07

Я бы не назвал это ошибкой, просто возможным ограничением дизайна. TS оценивает ObjOK более полно даже при его объявлении (наведите на него курсор), поэтому у него больше шансов оценить его достаточно в общих контекстах. Вы могли бы сообщить об этом как о предложении для поддержки какого-либо варианта использования, но я предполагаю, что они не захотят реализовывать какую-либо дополнительную работу по оценке общих функций, если только это не будет большой победой, и это, скорее всего, крайний случай.

jcalz 04.09.2024 00:19

Понятно, спасибо, оставлю как есть с фиксом расширения.

Magnus 04.09.2024 10:05

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

Похожие вопросы

Тип переменной Typescript не меняется даже при строго типизированных манипуляциях
Почему функции Contravariance с их параметрами в машинописном тексте?
Получите доступ к этому в функции, которая является свойством объекта
Создание сопоставлений на основе перечислений, включая обеспечение ошибок компилятора
Почему сопоставление массива не работает с той же записью сопоставления рекламы?
React Router обнаружил следующую ошибку во время рендеринга. Ошибка: отрисовано меньше перехватчиков, чем ожидалось
Почему машинописный текст не позволяет частично указывать аргументы типа, чтобы создать новую универсальную функцию из другой универсальной функции?
Компонент React со свойством «as», которое указывает на другой компонент и также наследует его свойства
Введите параметры из параметров функции
Как ввести динамический компонент в общий компонент Vue?