TS Type Challenges · Part 5 — Recursion over Tuples
Part 5: peel one element off and loop. Build Push, Unshift, Reverse, Includes, Flatten and Join with recursion over tuples, learn the accumulator pattern, and understand the recursion depth limit — each with a toggle-to-reveal answer.
Đây là Phần 5 của series 12 bài về TypeScript type challenges. Ở Phần 4 ta học cách bóc một phần tử ở hai đầu tuple. Đệ quy biến cú bóc đơn lẻ đó thành một vòng lặp — xử lý phần đầu, rồi đệ quy trên phần đuôi tới khi hết.
Ghi nguồn: các đáp án theo type-challenges (giấy phép MIT); số khớp ID chính thức.
Khuôn đệ quy
Mục tiêu
Ở runtime bạn duyệt danh sách bằng for, forEach, hoặc map. Ở tầng type không có từ khóa vòng lặp — bạn mô phỏng vòng lặp bằng cách gọi cùng một type alias trên input nhỏ hơn cho tới khi chạm điều kiện dừng. Đó là đệ quy ở tầng type.
Cách thử ngây thơ
Bản năng đầu tiên có thể là “đi” tuple bằng cách index từng vị trí:
// ❌ naive — you cannot loop over indices at the type level
type Reverse<T extends any[]> = [T[2], T[1], T[0]]; // only works for fixed length 3
Cách này hard-code ba vị trí và hỏng ngay khi tuple có độ dài khác. Bạn cần cơ chế chạy được với mọi độ dài mà không cần biết trước.
Cơ chế: bóc, xử lý, đệ quy
Mọi bài đệ quy tuple đều theo cùng khung:
type Loop<T extends any[]> = T extends [infer Head, ...infer Tail]
? /* do something with Head, then */ Loop<Tail> // recursive case
: /* base case (usually [] or '') */;
Đọc như if / else trên hình dạng của T:
| Piece | What it does | Runtime analogy |
|---|---|---|
T extends [infer Head, ...infer Tail] | Pattern-match: “does T have at least one element?” If yes, bind the first to Head and the rest to Tail | const [head, ...tail] = arr |
? … Loop<Tail> | Recursive case — do something with Head, then repeat on the smaller tuple Tail | Process head, call yourself on tail |
: … | Base case — T did not match the pattern (empty tuple []), so stop | arr.length === 0 → return |
Hai câu luôn phải trả lời trước khi viết logic: làm gì với Head? và base case nào dừng vòng lặp?
Từng dòng
type Loop<T extends any[]> = // 1️⃣ input must be an array/tuple type
T extends [infer Head, ...infer Tail] // 2️⃣ destructure: first element + rest
? Loop<Tail> // 3️⃣ recurse on the strictly smaller Tail
: never; // 4️⃣ base case: empty T → stop (placeholder)
- ràng buộc input để TypeScript biết đó là dạng tuple.
- destructure tuple variadic ở tầng type;
Headlà một phần tử,Tailluôn ngắn hơn một phần tử. - nhánh
?chỉ chạy khi pattern khớp (tuple không rỗng). - nhánh
:là base case — khiTlà[], pattern không khớp và đệ quy dừng.
Đánh giá cụ thể: đếm phần tử
Đây là bộ đếm tối thiểu để bạn xem compiler bung đệ quy:
type Length<T extends any[]> = T extends [infer _, ...infer Tail]
? Length<Tail> extends infer N extends number
? [1, ...N[]]['length']
: never
: 0;
type Demo = Length<[1, 2, 3]>; // 3
// step 0: Length<[1, 2, 3]>
// T = [1, 2, 3] → matches [Head, ...Tail] → Head=1, Tail=[2, 3]
// recurse → Length<[2, 3]>
// step 1: Length<[2, 3]>
// Head=2, Tail=[3]
// recurse → Length<[3]>
// step 2: Length<[3]>
// Head=3, Tail=[]
// recurse → Length<[]>
// step 3: Length<[]>
// [] does NOT match [Head, ...Tail] → base case → 0
// step 2 (unwinding): Length<[3]> = [1, ...0[]]['length'] = 1
// step 1 (unwinding): Length<[2, 3]> = [1, ...1[]]['length'] = 2
// step 0 (unwinding): Length<[1, 2, 3]> = [1, ...2[]]['length'] = 3
Compiler đi xuống tuple bóc từng head, chạm [], rồi bung ngược lên dựng kết quả. Mọi bài dưới đây là biến thể của làm gì với Head khi bung ngược.
Cạm bẫy
- Thiếu base case — nếu nhánh
:không bao giờ khớp[], TypeScript đệ quy tới giới hạn độ sâu rồi báo lỗi. - Tuple
readonly—[infer Head, ...infer Tail]cũng chạy trên tuplereadonly, nhưng một số bài cầnT extends readonly any[]trong ràng buộc. - Mảng không phải tuple —
any[]không có độ dài cố định có thể destructure không sạch; bài thường ràng buộcany[]hoặcreadonly any[]và dựa vào tuple literal trong test.
Tóm tắt: T extends [infer Head, ...infer Tail] ? …đệ quy Tail… : base là vòng for ở tầng type.
3057 · Push & 3060 · Unshift
Mục tiêu
Nối một type vào cuối tuple, hoặc chèn vào đầu — bản type-level của Array.prototype.push và Array.prototype.unshift.
type A = Push<[1, 2], 3>; // [1, 2, 3]
type B = Unshift<[1, 2], 0>; // [0, 1, 2]
Vì sao trông đơn giản nhưng đáng học
Hai bài này không cần đệ quy — nhưng là hạt cơ bản mọi lời giải đệ quy bên dưới dùng khi dựng accumulator hoặc tuple kết quả. Khi thấy [...Reverse<Tail>, Head] trong Reverse, spread đó chính là Push ngụy trang.
Cách thử ngây thơ
// ❌ naive — concatenation operator does not exist for types
type Push<T extends any[], V> = T + V; // syntax error
TypeScript không có + cho tuple; bạn phải dùng spread tuple.
Hiện đáp án
type Push<T extends any[], V> = [...T, V];
type Unshift<T extends any[], V> = [V, ...T];Từng dòng
Push
type Push<T extends any[], V> = [...T, V];
// ^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^
// T must be tuple-shaped spread append V at the endTlà kiểu tuple (hoặc mảng).- spread mọi phần tử của
T, rồi thêmVlàm phần tử cuối.
Unshift
type Unshift<T extends any[], V> = [V, ...T];
// ^ ^^^^
// V first, then all of TVđứng đầu; phần còn lại củaTtheo sau.
Đánh giá cụ thể
// Push<[1, 2], 3>
// step 0: T = [1, 2], V = 3
// step 1: spread T → 1, 2
// step 2: append V → [1, 2, 3]
// Unshift<[1, 2], 0>
// step 0: T = [1, 2], V = 0
// step 1: place V first → 0
// step 2: spread T after → [0, 1, 2]Cạm bẫy
- Bất biến — khác
pushruntime, đây trả về kiểu tuple mới; không có gì bị mutate. - Dùng trong đệ quy —
ReverseAcc<R, [H, ...Acc]>mỗi bướcUnshiftHvào accumulator.
Tóm tắt: Push = [...T, V], Unshift = [V, ...T] — khối dựng cho mọi mẫu accumulator.
3192 · Reverse
Mục tiêu
Đảo thứ tự các phần tử của tuple — [1, 2, 3] thành [3, 2, 1].
type R = Reverse<['a', 'b', 'c']>; // ['c', 'b', 'a']
Cách thử ngây thơ
// ❌ naive — prepending Head keeps original order
type Reverse<T extends any[]> = T extends [infer Head, ...infer Tail]
? [Head, ...Reverse<Tail>] // builds [first, second, third] — NOT reversed
: [];
Bóc Head rồi đặt đầu giữ nguyên thứ tự; bạn cần ngược lại — xử lý đuôi trước, rồi nối head vào cuối.
Cơ chế
Bóc Head, đảo hoàn toàn Tail, rồi đẩy Head vào cuối đuôi đã đảo. Phần tử bóc cuối cùng thành phần tử đầu trong kết quả vì nó chờ ở cuối qua mọi tầng đệ quy.
Hiện đáp án
type Reverse<T extends any[]> = T extends [infer Head, ...infer Tail]
? [...Reverse<Tail>, Head]
: [];Từng dòng
type Reverse<T extends any[]> = T extends [infer Head, ...infer Tail]
// ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// input tuple peel first element + rest
? [...Reverse<Tail>, Head]
// ^^^^^^^^^^^^^^^^^^^^^^^^
// recurse on Tail FIRST, spread that result, then append Head at the end
: [];
// ^^
// base case: empty tuple → empty tuple- Khớp pattern —
Tkhông rỗng thànhHead(đầu) +Tail(phần còn lại). - Nhánh đệ quy —
Reverse<Tail>chạy trước khi đặtHead; spread[...]nối đuôi đã đảo hoàn toàn vớiHeadở cuối. - Base case —
[]không khớp[Head, ...Tail], trả[].
Đánh giá cụ thể
// step 0: Reverse<[1, 2, 3]>
// Head = 1, Tail = [2, 3]
// need [...Reverse<[2, 3]>, 1] — must finish Reverse<[2, 3]> first
// step 1: Reverse<[2, 3]>
// Head = 2, Tail = [3]
// need [...Reverse<[3]>, 2]
// step 2: Reverse<[3]>
// Head = 3, Tail = []
// need [...Reverse<[]>, 3]
// step 3: Reverse<[]>
// [] does not match [Head, ...Tail] → base case → []
// step 2 (unwind): [...[], 3] = [3]
// step 1 (unwind): [...[3], 2] = [3, 2]
// step 0 (unwind): [...[3, 2], 1] = [3, 2, 1]Đặt Head sau kết quả đệ quy chính là thứ lật thứ tự — mỗi head chờ ở cuối trong khi các lời gọi trong giải quyết.
Cạm bẫy
- Không đệ quy đuôi —
[...Reverse<Tail>, Head]bọc lời gọi đệ quy trong spread, nên TypeScript không tối ưu đệ quy đuôi; tuple dài có thể chạm giới hạn ~50. - Sửa bằng accumulator —
ReverseAcc<T, Acc>prepend mỗiHeadvàoAcckhi đi xuống, biến lời gọi thành đệ quy đuôi (xem mục giới hạn độ sâu bên dưới).
Tóm tắt: đảo đuôi trước, đẩy head sau — [...Reverse<Tail>, Head].
898 · Includes
Mục tiêu
Tuple có chứa một type cho trước không? Như Array.prototype.includes — nhưng ở tầng type, với thành viên chính xác, không phải assignability lỏng.
type A = Includes<[1, 2, 3], 2>; // true
type B = Includes<[1, 2, 3], 4>; // false
type C = Includes<[boolean], false>; // false (not equal, just assignable)
Cách thử ngây thơ
// ❌ naive — extends checks assignability, not equality
type Includes<T extends readonly any[], U> = T extends [infer Head, ...infer Tail]
? Head extends U
? true
: Includes<Tail, U>
: false;
Với ca C, false extends boolean là true, nên cách này báo sai Includes<[boolean], false> là true. Bài cần bằng chính xác.
Cơ chế
Duyệt tuple từng Head. Mỗi bước so Head với U bằng Equal chặt từ Phần 3. Bước nào khớp → true; hết tuple mà không khớp → false.
Hiện đáp án
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? true
: false;
type Includes<T extends readonly any[], U> = T extends [infer Head, ...infer Tail]
? Equal<Head, U> extends true
? true
: Includes<Tail, U>
: false;Từng dòng
Equal<X, Y> — kiểm tra assignability hai chiều qua tham số hàm contravariant; trả true chỉ khi X và Y cùng type, không chỉ assignable.
Includes
type Includes<T extends readonly any[], U> = T extends [infer Head, ...infer Tail]
// peel one element
? Equal<Head, U> extends true
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// strict equality check — not Head extends U
? true
// ^^^^
// found it — short-circuit, stop searching
: Includes<Tail, U>
// ^^^^^^^^^^^^^^^^^
// not equal — keep searching in the rest
: false;
// ^^^^^
// empty tuple — never found U- Bóc
Headở đầu. - nếu bằng chặt, trả
truengay (không cần xét đuôi). - ngược lại đệ quy
Tail. - Tuple rỗng →
false.
Đánh giá cụ thể
// step 0: Includes<[1, 2, 3], 2>
// Head = 1, Tail = [2, 3]
// Equal<1, 2> → false → recurse Includes<[2, 3], 2>
// step 1: Includes<[2, 3], 2>
// Head = 2, Tail = [3]
// Equal<2, 2> → true → short-circuit: true
// result: trueĐánh giá cụ thể
// step 0: Includes<[boolean], false>
// Head = boolean, Tail = []
// Equal<boolean, false> → false (not equal — false is narrower)
// recurse Includes<[], false>
// step 1: Includes<[], false>
// [] → base case → false
// result: false ✅ correct (naive Head extends U would have returned true)Cạm bẫy
extendsvsEqual— dùngextendskhi ý là “assignable tới”; dùngEqualkhi test đòi đúng identity type.- Thoát sớm — trả
truekhi khớp nghĩa là không quét phần còn lại;falsechỉ khi hết cả tuple. readonly— ràng buộcreadonly any[]nhận cả input tuple mutable lẫn readonly.
Tóm tắt: bóc, Equal<Head, U>, short-circuit khi true, không thì đệ quy — không dùng extends thường cho thành viên chính xác.
459 · Flatten
Mục tiêu
Làm phẳng kiểu mảng lồng nhau sâu thành tuple một tầng — một chiều, mọi lá theo thứ tự.
type R = Flatten<[1, [2, [3, [4]], 5]]>; // [1, 2, 3, 4, 5]
Cách thử ngây thơ
// ❌ naive — only removes one level of nesting
type Flatten<T extends any[]> = T extends [infer Head, ...infer Tail]
? [...(Head extends any[] ? Head : [Head]), ...Flatten<Tail>]
: [];
Cách này bóc Head một tầng nhưng không đệ quy vào Head khi nó còn lồng — [2, [3, [4]]] vẫn còn lồng một phần. Bạn cần đệ quy hai chiều: dọc danh sách (Tail) và vào từng phần tử (Head).
Cơ chế
Mỗi bước bóc Head và hỏi: Head có phải mảng không?
- Có → làm phẳng
Headđệ quy, làm phẳngTailđệ quy, nối cả hai. - Không → giữ
Headnhư lá, chỉ làm phẳngTail.
Hiện đáp án
type Flatten<T extends any[]> = T extends [infer Head, ...infer Tail]
? Head extends any[]
? [...Flatten<Head>, ...Flatten<Tail>]
: [Head, ...Flatten<Tail>]
: [];Từng dòng
type Flatten<T extends any[]> = T extends [infer Head, ...infer Tail]
// peel Head + Tail
? Head extends any[]
// ^^^^^^^^^^^^^^^^^^
// branch: is Head nested?
? [...Flatten<Head>, ...Flatten<Tail>]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Head is array → flatten BOTH Head and Tail, concat
: [Head, ...Flatten<Tail>]
// ^^^^^^^^^^^^^^^^^^^^^^^^
// Head is leaf → keep Head, flatten only Tail
: [];
// base case: empty → []- Đệ quy ngoài —
T extends [infer Head, ...infer Tail]duyệt danh sách tầng trên. - Đệ quy trong —
Head extends any[]quyết định khoan vàoHeadhay coi là lá. - Nối — spread (
...) lắp lại các mảnh đã phẳng theo thứ tự. - Base case —
[]→[].
Đánh giá cụ thể
// step 0: Flatten<[1, [2, 3]]>
// Head = 1, Tail = [[2, 3]]
// 1 is NOT an array → [1, ...Flatten<[[2, 3]]>]
// step 1: Flatten<[[2, 3]]>
// Head = [2, 3], Tail = []
// [2, 3] IS an array → [...Flatten<[2, 3]>, ...Flatten<[]>]
// step 2: Flatten<[2, 3]>
// Head = 2, Tail = [3]
// 2 is NOT an array → [2, ...Flatten<[3]>]
// step 3: Flatten<[3]>
// Head = 3, Tail = []
// 3 is NOT an array → [3, ...Flatten<[]>]
// step 4: Flatten<[]>
// base case → []
// unwind step 3: [3, ...[]] = [3]
// unwind step 2: [2, ...[3]] = [2, 3]
// unwind step 1: [...[2, 3], ...[]] = [2, 3]
// unwind step 0: [1, ...[2, 3]] = [1, 2, 3]Cạm bẫy
- Đệ quy hai chiều — giới hạn độ sâu áp cho cả độ dài danh sách lẫn độ sâu lồng; lồng rất sâu có thể lỗi dù tuple ngắn.
any[]vs tuple —Head extends any[]khớp cả[2, 3]lẫnArray<number>; bài coi mọi kiểu mảng là có thể lồng.- Viết lại đệ quy đuôi — bài nâng cao cuối dùng accumulator để chịu lồng sâu hơn.
Tóm tắt: mỗi head, rẽ nhánh “có phải mảng?” — làm phẳng vào trong hoặc giữ làm lá, luôn đệ quy đuôi.
5310 · Join
Mục tiêu
Nối một tuple string/số thành một string, ngăn bởi U — như Array.prototype.join.
type A = Join<['a', 'p', 'p', 'l', 'e'], '-'>; // 'a-p-p-l-e'
type B = Join<['你', '好'], 1>; // '你1好'
Cách thử ngây thơ
// ❌ naive — always appends separator, leaving a trailing U
type Join<T extends (string | number)[], U extends string | number> =
T extends [infer Head, ...infer Tail]
? `${Head}${U}${Join<Tail, U>}`
: '';
// Join<['a', 'b'], '-'> → 'a-b-' ← trailing separator
Chèn U sau mọi Head tạo dấu ngăn thừa ở phần tử cuối. Bạn cần phát hiện “đây có phải phần tử cuối?”.
Cơ chế
Bóc Head, rồi kiểm tra Tail rỗng qua Tail['length'] extends 0.
- Phần tử cuối (
Taillà[]) → chỉ phát`${Head}`, không dấu ngăn. - Chưa cuối → phát
`${Head}${U}${Join<Tail, U>}`.
Dùng infer có ràng buộc (infer Head extends string | number) để Head hợp lệ trong template literal.
Hiện đáp án
type Join<
T extends (string | number)[],
U extends string | number,
> = T extends [
infer Head extends string | number,
...infer Tail extends (string | number)[],
]
? Tail['length'] extends 0
? `${Head}`
: `${Head}${U}${Join<Tail, U>}`
: '';Từng dòng
type Join<T extends (string | number)[], U extends string | number> =
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
// elements must be string | number separator too
T extends [
infer Head extends string | number,
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// constrained infer — Head is usable in `${Head}`
...infer Tail extends (string | number)[],
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// rest of tuple, also constrained
]
? Tail['length'] extends 0
// ^^^^^^^^^^^^^^^^^^^^^^^^
// is Tail empty? → Head is the LAST element
? `${Head}`
// ^^^^^^^^^
// last element: no separator after
: `${Head}${U}${Join<Tail, U>}`
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// not last: emit Head + separator + recurse
: '';
// ^^
// empty tuple → empty stringinfercó ràng buộc — không cóextends string | number,Headcó thể infer quá rộng cho template literal.Tail['length'] extends 0— độ dài tuple là literal type số;[]có length0.- Nhánh phần tử cuối — chỉ
`${Head}`, tránhUthừa cuối. - Nhánh đệ quy — dán
Head, dấu ngănU, và phần còn lại. - Base case — tuple rỗng →
''.
Đánh giá cụ thể
// step 0: Join<['a', 'b', 'c'], '-'>
// Head = 'a', Tail = ['b', 'c']
// Tail['length'] = 2, not 0 → `a-${Join<['b', 'c'], '-'>}`
// step 1: Join<['b', 'c'], '-'>
// Head = 'b', Tail = ['c']
// Tail['length'] = 1, not 0 → `b-${Join<['c'], '-'>}`
// step 2: Join<['c'], '-'>
// Head = 'c', Tail = []
// Tail['length'] = 0 → last element → `c`
// unwind step 1: `b-c`
// unwind step 0: `a-b-c`Đánh giá cụ thể
// step 0: Join<['你', '好'], 1>
// Head = '你', Tail = ['好']
// Tail['length'] = 1, not 0 → `你1${Join<['好'], 1>}`
// step 1: Join<['好'], 1>
// Head = '好', Tail = []
// Tail['length'] = 0 → `好`
// unwind: `你1好`Cạm bẫy
Tail['length'] extends 0vsTail extends []— cả hai phát hiện đuôi rỗng;lengthlà style type-challenges quen thuộc.- Kiểu dấu ngăn —
U extends string | numbercho phép dấu ngăn số như1trong ví dụ apple. - Tuple một phần tử —
Join<['x'], '-']>vào nhánh cuối ngay →'x', không phải'x-'.
Tóm tắt: bóc Head, kiểm Tail['length'] extends 0 cho phần tử cuối, không thì `${Head}${U}${Join<Tail, U>}`.
Ghi chú về giới hạn độ sâu đệ quy
Mục tiêu
Hiểu vì sao tuple dài đôi khi ra Type instantiation is excessively deep and possibly infinite — và mẫu accumulator sửa thế nào.
Vì sao có giới hạn
TypeScript giới hạn độ sâu khởi tạo type để chặn bung vô hạn từ type đệ quy lỗi. Trước đây giới hạn ~50 tầng; conditional type đệ quy đuôi có thể sâu hơn nhiều (~1000) vì compiler có thể gộp các lời gọi đệ quy liên tiếp giống nhau.
Không đệ quy đuôi vs đệ quy đuôi
// ❌ not tail-recursive: the recursive call is WRAPPED by [...Reverse<R>, H]
// TypeScript must fully resolve Reverse<R> before it can spread + append H
type Reverse<T extends any[]> = T extends [infer H, ...infer R]
? [...Reverse<R>, H]
: [];
// ✅ tail-recursive: the recursive call IS the entire result — nothing wraps it
type ReverseAcc<T extends any[], Acc extends any[] = []> = T extends [
infer H,
...infer R,
]
? ReverseAcc<R, [H, ...Acc]> // prepend H to accumulator, recurse
: Acc; // base case: return accumulator directly
Trong Reverse, mỗi tầng chờ kết quả trong, rồi bọc trong spread — đó không phải đệ quy đuôi. Trong ReverseAcc, lời gọi ReverseAcc<R, [H, ...Acc]> là toàn bộ đáp án của nhánh ?; không có constructor ngoài bọc nó.
Đánh giá cụ thể
// step 0: ReverseAcc<['a', 'b', 'c'], []>
// H='a', R=['b','c'], Acc=[] → ReverseAcc<['b','c'], ['a']>
// step 1: ReverseAcc<['b', 'c'], ['a']>
// H='b', R=['c'], Acc=['a'] → ReverseAcc<['c'], ['b', 'a']>
// step 2: ReverseAcc<['c'], ['b', 'a']>
// H='c', R=[], Acc=['b','a'] → ReverseAcc<[], ['c', 'b', 'a']>
// step 3: ReverseAcc<[], ['c', 'b', 'a']>
// [] → base case → Acc = ['c', 'b', 'a']
Đáp án được dựng hoàn toàn trong Acc khi base case chạy — return là tra cứu trực tiếp, không phải chuỗi spread lồng nhau.
Cạm bẫy
- Thứ tự accumulator —
ReverseAccprepend ([H, ...Acc]), nênAcclớn dần theo thứ tự đảo và đã đúng ở base case. - Không phải bài nào cũng đệ quy đuôi dễ —
Flattencần accumulator phức tạp hơn (xem bài nâng cao) vì đệ quy hai chiều. - Xem trước Phần 8 — số học tầng type ở Phần 8 dựa nhiều vào idiom accumulator này.
Tóm tắt: bọc lời gọi đệ quy → đau giới hạn độ sâu; để lời gọi đệ quy là việc cuối với accumulator → input sâu hơn nhiều.
Bài tập
1. Cài Without<T, U> xoá khỏi tuple T mọi phần tử có trong U (một giá trị hoặc tuple giá trị).
Lời giải
type ToUnion<U> = U extends any[] ? U[number] : U;
type Without<T extends any[], U> = T extends [infer H, ...infer R]
? H extends ToUnion<U>
? Without<R, U>
: [H, ...Without<R, U>]
: [];Cơ chế: chuẩn hoá U thành union qua ToUnion, rồi với mỗi Head hoặc bỏ (khi H extends ToUnion<U>) hoặc giữ và đệ quy.
// Without<[1, 2, 3, 4], [2, 4]>
// step 0: H=1, 1 extends 2|4? no → [1, ...Without<[2,3,4], [2,4]>]
// step 1: H=2, 2 extends 2|4? yes → skip → Without<[3,4], [2,4]>
// step 2: H=3, 3 extends 2|4? no → [3, ...Without<[4], [2,4]>]
// step 3: H=4, 4 extends 2|4? yes → skip → Without<[], [2,4]> → []
// unwind: [1, 3]2. Vì sao Includes cần Equal còn Without ở trên dùng extends cũng được?
Lời giải
Test của Without dùng literal khác biệt nên extends và Equal cho cùng kết quả — xoá “mọi thứ assignable tới 2 | 4” là ngữ nghĩa đúng. Includes có ca ([boolean] vs false) mà assignable ≠ equal: false extends boolean là true, nhưng Equal<boolean, false> là false. Luôn đọc test để biết bài cần thành viên (chính xác) hay lọc (assignable).
Nâng cao:viết lại Flatten kiểu đệ quy đuôi với accumulator để chịu được lồng rất sâu.
Điểm chính
- Khung lặp là
T extends [infer Head, ...infer Tail] ? …đệ quy Tail… : base. - Luôn chốt làm gì với
Headvà base case trước khi viết logic. - Dùng
Equal(khôngextends) khi bài cần thành viên chính xác. Flattenđệ quy hai chiều — vàoHeadvà dọcTail.- Với đầu vào dài, làm đệ quy đệ quy đuôi với accumulator để né giới hạn độ sâu.
Tiếp theo
Phần 6 — Thao tác Union: union không phải tuple, không index theo vị trí được. Ta biến string thành union, đếm và rút phần tử union, và dựng UnionToTuple nổi tiếng bằng đồ nghề contravariance từ Phần 1.