Мне нужно обеспечить сопоставление объекта. Цель состоит в том, чтобы создать type
или interface
, которые сопоставляются с ANIMAL_PLACE
на ANIMAL_TYPE
. Этот тип я затем хочу использовать для создания объекта из необходимых пар key:value
. Но мне нужно, чтобы компилятор выдавал ошибку, когда я забываю добавить ANIMAL_PLACE
или ANIMAL_TYPE
к объекту. Я могу сделать это только наполовину.
export enum ANIMAL_TYPE {
dog = "dog",
cat = "cat",
fish = "fish",
foo = "foo"
}
export enum ANIMAL_PLACE {
europe = 'europe',
africa = 'africa',
asia = "asia"
}
// This does not throw a compiler error when I forget to add ANIMAL_PLACE nor ANIMAL_TYPE to type
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
}
// this only throws a compiler error when I forget to add ANIMAL_PLACE to record
const AnimalPlaceToAnimalMapRecord: Record<ANIMAL_PLACE, ANIMAL_TYPE[]> = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish],
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog],
};
Вот Детская площадка
Если невозможно выдать ошибку компилятора при любом из двух изменений, есть ли способ получить ошибку компилятора, когда я забуду включить один из ANIMAL_TYPE
вместо ANIMAL_PLACE
...?
Вы можете написать тип для проверки предполагаемых требований, чтобы получать ошибки, если они не выполняются. См. ссылку на эту игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?
@jcalz это то, что я ищу. Не могли бы вы также объяснить в своем ответе, как использовать namespace
в качестве типа объекта?
«Как использовать namespace
в качестве типа объекта?» Я не уверен, о чем ты говоришь. Дело namespace
в внутреннем модуле, который позволяет мне писать одно и то же имя типа несколько раз без конфликтов. Это просто для демонстрации трех разных сценариев.
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
}
Этот тип объявлен так, чтобы соответствовать значению, которому он равен. Между ключами типа и значениями типа нет никакой связи.
Было бы яснее сказать, что тип — это то, каково его значение.
Благодаря этому у вас не возникнет никаких ошибок, поскольку тип правильный и из него не следует ничего делать.
Однако, когда вы объявляете Record<ANIMAL_PLACE, ANIMAL_TYPE[]>
, вы должны объявить что-то для всех записей ANIMAL_PLACE
.
В данном случае это не удается, потому что вы ничего не задекларировали для ключей ANIMAL_PLACE.asia
.
Вы можете обойти это, объявив свой const
как:
const AnimalPlaceToAnimalMapRecord: { [K in ANIMAL_PLACE]?: ANIMAL_TYPE[]} = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish],
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog],
};
Таким образом, все ключи являются необязательными. Однако это также означает, что вам придется выполнить некоторую проверку типов, поскольку вы не знаете, какие ключи используются, а какие нет.
Если вы хотите перевернуть его и гарантировать, что ошибка компилятора возникнет, когда вы забудете включить один из ANIMAL_TYPE
, вы можете сделать это следующим образом:
type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE[]>;
В определении type
? нет вывода типа?
Верно, почему-то я прочитал type AnimalPlaceToAnimalMapType = {...}
как type AnimalPlaceToAnimalMapType: {...}
, обновите ответ, чтобы он имел смысл, хороший улов.
Это type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE>;
требует ANIMAL_PLACE
, но мне нужно ANIMAL_PLACE[]
, что по сути то же самое, что и у меня const AnimalPlaceToAnimalMapRecord: Record<ANIMAL_PLACE, ANIMAL_TYPE[]> =...
, или я ошибаюсь?
Я обновил ответ, чтобы иметь type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE[]>
Они не одинаковы. Один использует ANIMAL_PLACE
как ключ к объекту, а другие — ANIMAL_TYPE
. Это означает, что их отношения перевернуты. В type AnimalTypeToAnimalPlaceMap
вы говорите, что каждое животное должно принадлежать хотя бы одному месту. В const AnimalPlaceToAnimalMapRecord
вы говорите, что в каждом месте есть x животных. Если рассуждать логически, они разные.
Вы можете написать служебный тип, требующий, чтобы два типа были «одинаковыми» (хотя тест, который я пишу, на самом деле просто требует, чтобы эти два типа были взаимоназначаемыми, что достаточно для ваших целей):
type Same<T extends U, U extends V, V = T> = void;
Это не дает ничего важного (Same<T, U>
всегда оценивается как void
), но ограничения на T
и U
делают так, что если вы пишете Same<T, U>
с разными типами, либо T
, либо U
вызовет ошибку компилятора:
type T1 = Same<string, string>; // okay
type T2 = Same<string, number>; // error!
// ~~~~~~
// Type 'string' does not satisfy the constraint 'number'.
type T3 = Same<"abc", string>; // error!
// ~~~~~~
// Type 'string' does not satisfy the constraint '"abc"'.
(Обратите внимание, что концептуально это будет type Same<T extends U, U extends T> = void
, но TypeScript отвергает это как незаконное циклическое ограничение. Обходной путь заключается в использовании U extends V
, где V
— параметр третьего типа, который по умолчанию имеет значение T
. Поэтому, когда вы используете Same<T, U>
, это похоже на написание Same<T, U, T>
, и тогда TS будет жаловаться, если U extends T
ложно.)
Итак, мы можем убедиться, что объединение ключей AnimalPlaceToAnimalMapType
идентично типу ANIMAL_PLACE
и что объединение типов элементов массива AnimalPlaceToAnimalMapType
идентично ANIMAL_TYPE
:
type VerifyMap =
Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> &
Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE>
Обратите внимание, что AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType]
— это объединение значений AnimalPlaceToAnimalMapType
, поскольку это индексированный доступ с объединением ключей. Мы ожидаем, что это будет объединение типов массива/кортежа, поэтому индексация его с помощью number
дает нам объединение их типов элементов.
Итак, если мы определили AnimalPlaceToAnimalMapType
правильно, то ошибок вы не получите:
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
[ANIMAL_PLACE.asia]: [ANIMAL_TYPE.foo]
}
type VerifyMap =
Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> &
Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE> // okay
Если вам не хватает элемента из ANIMAL_PLACE
, при первой проверке вы получите ошибку:
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog, ANIMAL_TYPE.foo]
}
type VerifyMap =
Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> & // error!
Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE>
А если вам не хватает элемента из ANIMAL_TYPE
, то при второй проверке вы получите ошибку:
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
[ANIMAL_PLACE.asia]: []
}
type VerifyMap =
Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> &
Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE> // error!
Это основной подход, который я бы использовал для наложения внеполосных ограничений на ваши типы. Вышеуказанного VerifyMap
может быть недостаточно для всех возможных проверок AnimalPlaceToAnimalMapType
; он проверяет отсутствие лишних вещей, но не запрещает повторы. Вы также можете написать проверку для этого, например, требуя, чтобы пересечение всех элементов массива значений свойств не перекрывалось:
type AnimalPlaceToAnimalMapType = {
[ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
[ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
[ANIMAL_PLACE.asia]: [ANIMAL_TYPE.foo, ANIMAL_TYPE.cat], // repeat
}
type APAMT = AnimalPlaceToAnimalMapType;
type ISect = { [K in keyof APAMT]: { [P in keyof APAMT]:
P extends K ? never : APAMT[P][number] & APAMT[K][never]
}[keyof APAMT] }[keyof APAMT]
type VerifyMap =
Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> &
Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE> &
Same<ISect, never>; // error!
Я не буду вдаваться в подробности того, как это работает, поскольку это выходит за рамки заданного вопроса. Дело в том, что обычно вы можете написать какой-нибудь служебный тип для проверки инвариантов, которые хотите применить.
Детская площадка, ссылка на код
Ух ты... Еще так много нужно узнать о типах в TS. Большое спасибо
Однако один вопрос. Я понимаю этот момент в Same<T extends U, U extends V>
, но почему V = T
?
Так было бы T extends U, U extends T
в идеальном мире, но это противозаконно. Я отредактировал ответ, чтобы объяснить этот момент.
Возможно, вы ищете какое-то статическое утверждение, которое
ANIMAL_TYPE
расширяетсяkeyof AnimalPlaceToAnimalMapType
и чтоANIMAL_PLACE extends AnimalPlaceToAnimalMapType[ANIMAL_TYPE][number]
?