AnyOf
Завдання
Реалізувати функцію any із мови Python на рівні типів. Тип приймає кортеж й
повертає true, якщо будь-який із елементів кортежу true. Якщо ж кортеж
порожній, то повертаємо false. Наприклад:
type Sample1 = AnyOf<[1, "", false, [], {}]>; // expected to be true
type Sample2 = AnyOf<[0, "", false, [], {}]>; // expected to be false
Розв’язок
Моя перша ідея була використати дистрибутивні умовні типи.
Ми можемо використати синтаксис T[number] для того, щоб взяти об’єднання всіх
елементів з кортежу. Маючи об’єднання елементів, переберемо кожен із них, й на
кожній ітерації будемо повертати false або true в залежності від типу
елемента. У випадку, якщо всі ітерації повернуть false, ми отримаємо тип
false. Але, якщо у нас буде хоч один true, то ми отримаємо boolean. Тому
що false | true = boolean. Перевіряючи що ми отримали в результаті, false чи
boolean, ми можемо зрозуміти, чи є true в об’єднанні.
Але, як виявилося, реалізація такого підходу виглядає не дуже добре. Оцініть самі:
type AnyOf<T extends readonly any[], I = T[number]> = (
I extends any ? (I extends Falsy ? false : true) : never
) extends false
? false
: true;
Тому я задумався, а чи можемо ми вирішити це простішим способом? Щоб, хоч якось, можна було зрозуміти, що відбувається, і не загубитися в майбутньому. Виявляється, ми можемо!
Давайте згадувати про виведення типів в кортежах і про варіативні типи. Пам’ятаєте, ми їх використовували в розв’язках таких задач як Last чи Pop і їм подібні.
Почнемо з виведення одного елементу із кортежу й решти, що є в хвості:
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? never
: never;
Як ми можемо перевірити, що наший H це false? Для початку, давайте створимо
новий тип, в якому вкажемо елементи, які ми вважаємо false:
type Falsy = 0 | "" | false | [] | { [P in any]: never };
Маючи такий тип, ми можемо використати умовний тип для перевірки, чи входить H
в їх число:
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? never
: never
: never;
Що нам робити, якщо тип виявився false? Для нашого завдання це означає, що
true ще не знайдено й ми продовжимо його шукати. Реалізуємо це через
рекурсивний виклик типу з хвостом із кортежу, в якості аргументу.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: never
: never;
Підходимо все ближче до розв’язку проблеми. Що якщо елемент H це не false?
Тоді це true, ми знайшли його! Якщо ми знайшли true в кортежі, то
продовжувати немає сенсу й ми просто повертаємо true.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: never;
Останній стан, який у ми ще не опрацювали - порожній кортеж. У випадку з
порожнім кортежом, наше виведення не спрацює. В такому випадку, як і вказано в
умові завдання, ми повертаємо false.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: false;
Таким чином ми реалізували функцію any з Python на рівні системи типів у
TypeScript. Ось повний розв’язок завдання:
type Falsy = 0 | "" | false | [] | { [P in any]: never };
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: false;
Коментарі