Рассмотрим приведенный ниже код:
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 Готово. Надеюсь, это стало немного яснее. Спасибо за руководство.
Отлично, теперь мы можем рассмотреть проблему. К сожалению, TS не очень хорошо разбирается в общей логике высшего порядка. Можно было бы надеяться, что он сможет распознать, как (X & Y)[K]
присваивается X[K]
для общего X
, но это не так. Лучшее, что вы можете сделать, это расширить, чтобы избавиться от пересечения перед индексацией, как показано в этой ссылке на игровую площадку. Это полностью отвечает на заданный вопрос? Если да, то я напишу ответ; если нет, то что мне не хватает?
@jcalz Отлично. Да, это решает проблему. Меня такое поведение удивляет. Возможно, TS расширяется (X & Y)[K]
до (X)[K]
| (Y)[K]
, который нельзя назначить (X)[K]
....
Скорее, он вообще не пытается его расширять или проверять. Многие операции с универсальными типами фактически откладываются. Я напишу ответ, когда у меня будет возможность.
Я не вижу хорошей документации по конкретной вещи, которую вы здесь видите (самая близкая, которую я нашел, это 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
является объектом. Что вы думаете? Стоит ли сообщать об ошибке?
Я бы не назвал это ошибкой, просто возможным ограничением дизайна. TS оценивает ObjOK
более полно даже при его объявлении (наведите на него курсор), поэтому у него больше шансов оценить его достаточно в общих контекстах. Вы могли бы сообщить об этом как о предложении для поддержки какого-либо варианта использования, но я предполагаю, что они не захотят реализовывать какую-либо дополнительную работу по оценке общих функций, если только это не будет большой победой, и это, скорее всего, крайний случай.
Понятно, спасибо, оставлю как есть с фиксом расширения.
@jcalz Спасибо, что присоединились. Рад попробовать, но на самом деле я не понимаю, почему это не работает. Если я удалю
& { [P in TypeSelection]: InnerObject<P> }
из Obj, ошибка исчезнет, но тогда я перенесу проблему наdefaultObj
, так как теперь он не знает, чтоone
существует на объекте. Я пытаюсь заставить работать аналогичную, но более сложную функцию из реальной жизни, поэтому ОП был моей первой попыткой MRE. Вопрос в том, как заставить эту функцию работать, но я не знаю, в чем заключается основная проблема/вопрос.