Кортеж, который принимает только одно вхождение определенного типа

Я пытаюсь написать функцию, которая принимает кортеж типа объединения, но конкретный член объединения должен появляться только один раз (или не появляться вообще), например. [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 отдельно, он работает как положено, но на пересечении ограничение не имеет никакого эффекта.

Ваш вопрос: «Что не так с моей неудачной попыткой X»? Если да, пожалуйста, отредактируйте , чтобы задать вопрос именно об обнаружении проблемы с вашей неудачной попыткой... а именно, параметры константного типа будут иметь тенденцию выводить readonly кортежи, а ваш EnsureOne их не ожидает. Это легко исправить (если да, то я мог бы написать ответ). Или ваш вопрос: «Как мне написать X»? Если да, то, пожалуйста, отредактируйте, чтобы удалить неудавшуюся попытку, и кто-нибудь сможет предложить независимое решение.

jcalz 18.06.2024 21:55

Я думаю, мой вопрос должен был быть в первую очередь первым, но был бы немного открытым, если бы был возможен другой путь. Не совсем уверен, как это перефразировать. Извините, английский не мой родной язык. Но по сути вы уже дали ответ, спасибо за ссылку на игровую площадку! Если вы напишете это как ответ, я приму это.

Tao 18.06.2024 22:35

Не совсем часть этого вопроса, но я изменил решение, связанное с Тицианом, на игровой площадке, чтобы использовать параметр константного типа вместо параметра отдыха, и оно все равно работало без необходимости использования readonly в части extends [infer Head, ...infer Tail] (Ссылка на игровую площадку). Так что мне до сих пор интересно, зачем это требуется в моем случае.

Tao 18.06.2024 22:44

потому что ты поставил readonly Union[] вместо Union[]. Я напишу ответ, когда у меня будет возможность.

jcalz 18.06.2024 23:14
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Ваш код должен либо использовать 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 действительно отлично подходит для более сложных ограничений типа, подобных этим.

Tao 19.06.2024 09:35

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