Определите тип только с одним свойством, и имя этого свойства должно быть в предопределенном наборе имен.

Допустим, я хочу создать структуру ошибок, которая указывает, где ошибка — на стороне сервера или на стороне клиента. Пример:

const serverErr = {
  server: "Error!"
};

const clientErr = {
  client: "Error!"
};

Объект ошибки должен иметь только одно свойство, и имя этого свойства должно быть server или client.

Я пытался ответить на этот вопрос вопрос, но он не работает.

Согласно этому ответу, вот определение IsSingleKey:

export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
    ? I
    : never;
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type ISingleKey<K extends string, T> = IsUnion<K> extends true ? "Can only contain a single key" : Record<K, T>;

Попробуйте ISingleKey<"server", string> | ISingleKey<"client", string>

Bergi 19.02.2023 11:58

Что, если есть 20 имен, которые могут подойти? Я не хочу объединять 20 "ISingleKey" вместе, и это не годится для обслуживания.

Trí Phan 19.02.2023 13:02

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

jcalz 19.02.2023 17:54

Я использую это решение, чтобы обойти эту проблему прямо сейчас. Однако, когда я работаю с данными, мне приходится выполнять дополнительную работу.

Trí Phan 20.02.2023 03:52

Когда вы говорите, что используете «это решение», вы имеете в виду что-то вроде этого? Или вы имеете в виду что-то другое? Я с удовольствием напишу эту технику в качестве ответа, но только если она соответствует вашим потребностям.

jcalz 20.02.2023 04:44

О, да, по этой ссылке есть ответ, который я искал. Я думал, вы сказали что-то об использовании Record с объединением имен в качестве ключей.

Trí Phan 20.02.2023 11:15
Что такое двойные вопросительные знаки (??) в JavaScript?
Что такое двойные вопросительные знаки (??) в JavaScript?
Как безопасно обрабатывать неопределенные и нулевые значения в коде с помощью Nullish Coalescing
Асинхронная передача данных с помощью sendBeacon в JavaScript
Асинхронная передача данных с помощью sendBeacon в JavaScript
В современных веб-приложениях отправка данных из JavaScript на стороне клиента на сервер является распространенной задачей. Одним из популярных...
Принципы ООП в JavaScript
Принципы ООП в JavaScript
Парадигма объектно-ориентированного программирования имеет 4 основных принципа,
Laravel с Turbo JS
Laravel с Turbo JS
Turbo - это библиотека JavaScript для упрощения создания быстрых и высокоинтерактивных веб-приложений. Она работает с помощью техники под названием...
Слишком много useState? Давайте useReducer!
Слишком много useState? Давайте useReducer!
Современный фронтенд похож на старую добрую веб-разработку, но с одной загвоздкой: страница в браузере так же сложна, как и бэкенд.
Типы данных JavaScript
Типы данных JavaScript
В JavaScript существует несколько типов данных, включая примитивные типы данных и ссылочные типы данных. Вот краткое объяснение различных типов данных...
1
6
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Один из подходов состоит в том, чтобы создать тип ошибки AllowedErrors, который является объединением всех допустимых типов, каждый член которого имеет ровно один из требуемых допустимых ключей и запрещает определенные значения для других ключей. Это будет выглядеть так:

type AllowedErrors = 
  { server: string; client?: never; x?: never; y?: never; z?: never; } | 
  { client: string; server?: never; x?: never; y?: never; z?: never; } | 
  { x: string; server?: never; client?: never; y?: never; z?: never; } | 
  { y: string; server?: never; client?: never; x?: never; z?: never; } | 
  { z: string; server?: never; client?: never; x?: never; y?: never; };

Обратите внимание, что TypeScript явно не поддерживает запрет ключа свойства, но вы можете сделать его необязательным свойством , тип значения которого невозможно никогда не набирать, тогда единственным значением, которое вы можете найти в этом ключе, будет undefined.

Это должно вести себя так, как вы хотите:

const serverErr: AllowedErrors = {
    server: "Error!"
};

const clientErr: AllowedErrors = {
    client: "Error!"
};

const bothErr: AllowedErrors = {
    server: "Error",
    client: "Error"
} // error! Type '{ server: string; client: string; }' 
// is not assignable to type 'AllowedErrors'

const neitherErr: AllowedErrors = {
    oops: "Error"
} // Type '{ oops: string; }' is not assignable 
// to type 'AllowedErrors'.

const okayErr = Math.random() < 0.5 ? { x: "" } : { y: "" };

Ну и замечательно.


Но, очевидно, вы не хотите писать или изменять свой тип AllowedErrors вручную. Вы хотите сгенерировать его из объединения ключей, например

type AllowedErrorKeys = "server" | "client" | "x" | "y" | "z";

Так что вам просто нужно добавить/удалить вещи там. Ну, вот один из подходов:

type SingleKeyValue<K extends PropertyKey, V, KK extends PropertyKey = K> =
    K extends PropertyKey ?
    Record<K, V> & { [P in Exclude<KK, K>]?: never } extends infer O ?
    { [P in keyof O]: O[P] } : never : never;    

Тип SingleKeyValue<K, V> создает соответствующее объединение, в котором каждый член имеет только один разрешенный ключ из K с типом значения V, а остальные ключи из K запрещены.

Во-первых, тип обернут кажущимся неоперативным K extends PropertyKey ? ... : never, который на самом деле является дистрибутивным условным типом, который разбивает K на отдельные члены объединения, работает с каждым членом, а затем объединяет результаты обратно в другое объединение.

Внутренний тип по сути Record<K, V> & { [P in Exclude]?: never }; первая часть с использованием служебного типа Record<K, V> для представления объекта с одним ключом из K и типом значения V, который пересекается с типом, запрещающим все ключи из Exclude<KK, K> с использованием Исключить тип утилиты <T, U> . Подождите, что такое KK? Это параметр «фиктивного» типа, который я объявил во внешней области видимости, который по умолчанию равен K. На самом деле это просто трюк, чтобы K разбивалось на части объединения, а KK являлось копией исходного объединения K, чтобы мы могли выразить «все члены исходного объединения, кроме одного».

В любом случае, это тип, который нам нужен, но он довольно уродлив (пересечение служебных типов), поэтому я использую метод, описанный в Как я могу увидеть полный расширенный контракт типа Typescript? , ... extends infer O ? { [P in keyof O]: O[P] } : never, чтобы скопировать тип в другой параметр типа и выполнить итерацию его свойств с сопоставленным типом, чтобы получить один тип объекта.


И теперь мы можем просто написать

type AllowedErrors = SingleKeyValue<AllowedErrorKeys, string>;

И используйте IntelliSense, чтобы убедиться, что он оценивает

/* type AllowedErrors = 
  { server: string; client?: never; x?: never; y?: never; z?: never; } | 
  { client: string; server?: never; x?: never; y?: never; z?: never; } | 
  { x: string; server?: never; client?: never; y?: never; z?: never; } | 
  { y: string; server?: never; client?: never; x?: never; z?: never; } | 
  { z: string; server?: never; client?: never; x?: never; y?: never; }*/

По желанию.

Площадка ссылка на код

Ох уж эти ужасные уловки, вот почему я ненавижу TypeScript. +1 за их объяснение!

Bergi 20.02.2023 17:30

Вау, спасибо за ваш обстоятельный ответ. Техника, используемая для этого, довольно сложна. А теперь я вижу, как используется тип never.

Trí Phan 21.02.2023 01:37

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