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()
у
системі типів!
Коментарі