Chunk
Завдання
Ви знаєте lodash? Chunk — дуже корисна функція, давайте її реалізуємо.
Chunk<T, N> приймає два обов’язкові тип-параметри. T має бути кортежем, а
N має бути цілим числом >= 1. Наприклад:
type R0 = Chunk<[1, 2, 3], 2>; // expected to be [[1, 2], [3]]
type R1 = Chunk<[1, 2, 3], 4>; // expected to be [[1, 2, 3]]
type R2 = Chunk<[1, 2, 3], 1>; // expected to be [[1], [2], [3]]
Розв’язок
Це завдання було міцним горішком. Але врешті-решт я знайшов рішення, яке легко зрозуміти, як на мене. Ми починаємо з порожнього типу, який описує контракт:
type Chunk<T, N> = any;
Оскільки нам потрібно накопичувати фрагменти кортежу, здається доцільним мати
необов’язковий тип-параметр A, який накопичуватиме фрагмент розміром N. За
замовчуванням тип-параметр A буде порожнім кортежем:
type Chunk<T, N, A extends unknown[] = []> = any;
Маючи порожній акумулятор, який ми будемо використовувати як тимчасовий
фрагмент, ми можемо почати розділяти T на частини - перший елемент кортежу та
решту:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? never
: never;
Маючи частини кортежу T, ми можемо перевірити, чи акумулятор необхідного
розміру. Для цього, ми перевіряємо властивість length. Це працює, тому що ми
маємо обмеження на тип-параметр A, яке говорить, що це кортеж.
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? never
: never
: never;
Якщо акумулятор порожній або в ньому недостатньо елементів, нам потрібно
продовжувати розділяти T, доки він не матиме потрібного розміру. Для цього ми
продовжуємо рекурсивно викликати тип Chunk із новим акумулятором. Ми передаємо
в нього A та елемент H з T:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? never
: Chunk<T, N, [...A, H]>
: never;
Рекурсивний виклик продовжується до тих пір, поки ми не отримаємо акумулятор
необхідного розміру N. Це наш перший фрагмент, який нам потрібно повернути в
результаті. Отже, ми повертаємо новий кортеж із акумулятором у ньому:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A]
: Chunk<T, N, [...A, H]>
: never;
При цьому ми ігноруємо решту кортежу T. Отже, нам потрібно додати ще один
рекурсивний виклик до нашого результату [A], який очистить акумулятор і почне
той самий процес:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<T, N>]
: Chunk<T, N, [...A, H]>
: never;
Ця рекурсивна магія триває, доки в кортежі T не буде елементів. У такому
випадку ми просто повертаємо все, що залишилося в акумуляторі. Це потрібно, тому
що ми можемо мати випадок, коли розмір акумулятора буде меншим за N, й ми
втратимо елементи якщо його не повернемо.
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<T, N>]
: Chunk<T, N, [...A, H]>
: [A];
Є ще один випадок, коли втрачається елемент H. Коли ми отримали акумулятор
необхідного розміру, ми ігноруємо виведений H. Щоб це виправити, нам потрібно
передавати H в наступний Chunk:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: [A];
Це рішення працює для деяких тестів, що чудово. Однак у нас є випадок, коли
рекурсивний виклик типу Chunk повертає кортежі в кортежі в кортежі (через
рекурсивні виклики). Щоб виправити це, давайте додамо ... до нашого
Chunk<[H, ...T], N>:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, ...Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: [A];
Всі тести пройдено! Ура… крім граничного випадку з порожнім кортежем. Це лише граничний випадок, і ми можемо додати умовний тип, щоб його покрити. Якщо акумулятор порожній у базовому випадку, ми повертаємо порожній кортеж. В іншому випадку повертаємо сам акумулятор, як і раніше:
type Chunk<T, N, A extends unknown[] = []> = T extends [infer H, ...infer T]
? A["length"] extends N
? [A, ...Chunk<[H, ...T], N>]
: Chunk<T, N, [...A, H]>
: A[number] extends never
? []
: [A];
Це все, що нам потрібно, щоб реалізувати lodash-версію функції .chunk() у
системі типів!
Коментарі