Допустим, я хочу создать структуру ошибок, которая указывает, где ошибка — на стороне сервера или на стороне клиента. Пример:
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>;
Что, если есть 20 имен, которые могут подойти? Я не хочу объединять 20 "ISingleKey" вместе, и это не годится для обслуживания.
Вы не можете запретить дополнительные свойства в TypeScript; все, что вы можете сделать, это отговорить их. Примете ли вы решение, которое берет объединение имен ключей и создает тип, требующий, чтобы ровно один из этих ключей имел определенное свойство?
Я использую это решение, чтобы обойти эту проблему прямо сейчас. Однако, когда я работаю с данными, мне приходится выполнять дополнительную работу.
Когда вы говорите, что используете «это решение», вы имеете в виду что-то вроде этого? Или вы имеете в виду что-то другое? Я с удовольствием напишу эту технику в качестве ответа, но только если она соответствует вашим потребностям.
О, да, по этой ссылке есть ответ, который я искал. Я думал, вы сказали что-то об использовании Record с объединением имен в качестве ключей.
Один из подходов состоит в том, чтобы создать тип ошибки 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
В любом случае, это тип, который нам нужен, но он довольно уродлив (пересечение служебных типов), поэтому я использую метод, описанный в Как я могу увидеть полный расширенный контракт типа 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 за их объяснение!
Вау, спасибо за ваш обстоятельный ответ. Техника, используемая для этого, довольно сложна. А теперь я вижу, как используется тип never.
Попробуйте ISingleKey<"server", string> | ISingleKey<"client", string>