Создание сопоставлений на основе перечислений, включая обеспечение ошибок компилятора

Мне нужно обеспечить сопоставление объекта. Цель состоит в том, чтобы создать 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...?

Возможно, вы ищете какое-то статическое утверждение, которое ANIMAL_TYPE расширяется keyof AnimalPlaceToAnimalMapType и что ANIMAL_PLACE extends AnimalPlaceToAnimalMapType[ANIMAL_TYPE][number]?

Bergi 30.08.2024 11:23

Вы можете написать тип для проверки предполагаемых требований, чтобы получать ошибки, если они не выполняются. См. ссылку на эту игровую площадку. Это полностью решает вопрос? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 30.08.2024 15:19

@jcalz это то, что я ищу. Не могли бы вы также объяснить в своем ответе, как использовать namespace в качестве типа объекта?

four-eyes 31.08.2024 11:11

«Как использовать namespace в качестве типа объекта?» Я не уверен, о чем ты говоришь. Дело namespace в внутреннем модуле, который позволяет мне писать одно и то же имя типа несколько раз без конфликтов. Это просто для демонстрации трех разных сценариев.

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

Ответы 2

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? нет вывода типа?

Bergi 30.08.2024 11:20

Верно, почему-то я прочитал type AnimalPlaceToAnimalMapType = {...}как type AnimalPlaceToAnimalMapType: {...}, обновите ответ, чтобы он имел смысл, хороший улов.

IDK4real 30.08.2024 11:28

Это type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE>; требует ANIMAL_PLACE, но мне нужно ANIMAL_PLACE[], что по сути то же самое, что и у меня const AnimalPlaceToAnimalMapRecord: Record<ANIMAL_PLACE, ANIMAL_TYPE[]> =..., или я ошибаюсь?

four-eyes 30.08.2024 11:32

Я обновил ответ, чтобы иметь type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE[]> Они не одинаковы. Один использует ANIMAL_PLACE как ключ к объекту, а другие — ANIMAL_TYPE. Это означает, что их отношения перевернуты. В type AnimalTypeToAnimalPlaceMap вы говорите, что каждое животное должно принадлежать хотя бы одному месту. В const AnimalPlaceToAnimalMapRecord вы говорите, что в каждом месте есть x животных. Если рассуждать логически, они разные.

IDK4real 30.08.2024 11:38
Ответ принят как подходящий

Вы можете написать служебный тип, требующий, чтобы два типа были «одинаковыми» (хотя тест, который я пишу, на самом деле просто требует, чтобы эти два типа были взаимоназначаемыми, что достаточно для ваших целей):

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. Большое спасибо

four-eyes 02.09.2024 07:51

Однако один вопрос. Я понимаю этот момент в Same<T extends U, U extends V>, но почему V = T?

four-eyes 02.09.2024 08:00

Так было бы T extends U, U extends T в идеальном мире, но это противозаконно. Я отредактировал ответ, чтобы объяснить этот момент.

jcalz 02.09.2024 15:53

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