Я пытаюсь написать функцию, которая принимает кортеж типа объединения, но конкретный член объединения должен появляться только один раз (или не появляться вообще), например. [A, B, A]
действителен, а [A, B, A, B]
нет. В идеале я мог бы просто написать что-то вроде [...A[], B, ...A[]] | A[]
, но TypeScript этого не позволяет.
В поисках возможных решений я нашел этот ответ на немного другую, но связанную с ним проблему: применение сложного ограничения к типу кортежа. Решение по сути пересекает кортеж с универсальным типом, который разрешается либо в unknown
, либо в never
. С помощью рекурсии можно проверить каждый член кортежа.
К сожалению, моя попытка, похоже, не сработала, и я не знаю, почему. Видимо, я не слишком хорошо понимаю решение связанного вопроса. Пример кода ниже:
type A = number;
type B = string;
type EnsureOne<Tuple, T, Seen = false> =
Tuple extends [infer Head, ...infer Tail] ?
Head extends T ?
Seen extends true ?
{ ERROR: [`Only one value of type`, T, `is allowed`] } :
EnsureOne<Tail, T, true> :
EnsureOne<Tail, T, Seen> :
unknown;
type ExpectUnknown = EnsureOne<[A, B, A], B>;
// ^? unknown
type ExpectError = EnsureOne<[A, B, A, B], B>;
// ^? { ERROR: ["Only one value of type", string, "is allowed"] }
type Union = A | B;
declare function f<const T extends readonly Union[]>(t: T & EnsureOne<T, B>): void;
const expectError = f([0, '', '']); // no error :(
Когда я использую EnsureOne
отдельно, он работает как положено, но на пересечении ограничение не имеет никакого эффекта.
Я думаю, мой вопрос должен был быть в первую очередь первым, но был бы немного открытым, если бы был возможен другой путь. Не совсем уверен, как это перефразировать. Извините, английский не мой родной язык. Но по сути вы уже дали ответ, спасибо за ссылку на игровую площадку! Если вы напишете это как ответ, я приму это.
Не совсем часть этого вопроса, но я изменил решение, связанное с Тицианом, на игровой площадке, чтобы использовать параметр константного типа вместо параметра отдыха, и оно все равно работало без необходимости использования readonly
в части extends [infer Head, ...infer Tail]
(Ссылка на игровую площадку). Так что мне до сих пор интересно, зачем это требуется в моем случае.
потому что ты поставил readonly Union[]
вместо Union[]
. Я напишу ответ, когда у меня будет возможность.
Ваш код должен либо использовать readonly
, либо нет, но быть последовательным.
Ваша f()
функция является универсальной в параметре константного типа T
, который ограничен типом массива только для чтения readonly Union[]
. Это означает, что когда вы вызываете его как f([0, "a", "b"])
, компилятор выведет аргумент типа для T
как тип кортежа только для чтения readonly [0, "a", "b"]
.
Но ваш тип EnsureOne
не ожидает, что его первый аргумент типа будет кортежем readonly
. Вы сравниваете его с общим кортежем с вариационным типом кортежа[infer Head, ...infer Tail]
. Они не совпадают. Так становится unknown
и ничего не отвергается.
Вы можете исправить EnsureOne
, разрешив readonly
:
type EnsureOne<Tuple, T, Seen = false> =
Tuple extends readonly [infer Head, ...infer Tail] ?
// ^^^^^^^^
Head extends B ?
Seen extends true ?
{ ERROR: [`Only one value of type`, T, `is allowed`] } :
EnsureOne<Tail, T, true> :
EnsureOne<Tail, T, Seen> :
unknown;
или вы можете исправить f()
и запретить ограничение readonly
:
declare function f<const T extends Union[]>(t: T & EnsureOne<T, B>): void;
или вы можете сделать и то, и другое. В любом случае вы получите желаемое поведение:
declare function f<const T extends readonly Union[]>(t: T & EnsureOne<T, B>): void;
f([0, "a", "b"]) // error!
// Property 'ERROR' is missing in type '[0, "a", "b"]' but required
// in type '{ ERROR: ["Only one value of type", string, "is allowed"]; }'.
f([0, 1, 2]); // okay
f([0, 1, 2, "a"]); // okay
f(["a", "b"]); // error!
Другой подход
Или вы можете переписать свой код, чтобы полностью избежать рекурсивных условных типов . Один из способов сделать это — использовать тип сопоставленного массива , который сравнивает I
-й элемент T
со всеми остальными элементами, и если он когда-либо обнаруживает, что, например, оба I
-го и J
-го элемента являются назначаемыми. до B
(или любого другого типа, который вы хотите проверить), если J
и I
различны, тогда этот элемент ошибочен. Элементы ошибок могут быть сопоставлены с типом «никогда» или чем-то дурацким, например { ERROR: ⋯ }
(предположительно, чтобы вести себя как «недопустимый» тип, как описано в microsoft/TypeScript#23689). В противном случае оно останется в покое.
Возможно, вот так:
declare function g<T extends Union[]>(t: [...{ [I in keyof T]:
T[I] extends B ? (
unknown extends {
[J in keyof T]: I extends J ? never : T[J] extends B ? unknown : never
}[number] ? never : T[I]
) : T[I] }]): void;
g([0, "a", "b"]); // error!
g([0, 1, 2]); // okay
g([0, 1, 2, "a"]); // okay
g(["a", "b"]); // error!
Это также ведет себя по желанию. Я не буду вдаваться в подробности того, как это работает, главным образом потому, что, похоже, вопрос действительно заключался в исправлении исходного подхода, а не в реализации другого.
Детская площадка, ссылка на код
Спасибо! Очень проницательно. Я думаю, вы уже достаточно объяснили альтернативный подход. Тип invalid
действительно отлично подходит для более сложных ограничений типа, подобных этим.
Ваш вопрос: «Что не так с моей неудачной попыткой X»? Если да, пожалуйста, отредактируйте , чтобы задать вопрос именно об обнаружении проблемы с вашей неудачной попыткой... а именно, параметры константного типа будут иметь тенденцию выводить
readonly
кортежи, а вашEnsureOne
их не ожидает. Это легко исправить (если да, то я мог бы написать ответ). Или ваш вопрос: «Как мне написать X»? Если да, то, пожалуйста, отредактируйте, чтобы удалить неудавшуюся попытку, и кто-нибудь сможет предложить независимое решение.