TS Type Challenges · Part 8 — Type-level Arithmetic
Part 8: TypeScript has no + for numbers, so we count with tuple lengths. Build BuildTuple, Add, Subtract, GreaterThan and Fibonacci using the accumulator and tuple-length tricks — each with a toggle-to-reveal answer and explanation.
Đây là Phần 8 của series 12 bài về TypeScript type challenges. Hệ thống type không có toán tử số học — bạn không viết được A + B trên number literal. Cách lách rất đẹp: một tuple độ dài N chính là số N, qua ['length']. Nên ta làm toán bằng cách dựng, trải, và đo tuple.
Ghi nguồn: các đáp án theo type-challenges (giấy phép MIT); số khớp ID chính thức.
Nền tảng — BuildTuple
Mục tiêu
Gần như mọi bài số học bắt đầu bằng việc biến một số thành tuple có độ dài đó. Ở runtime bạn viết Array(3).fill(0) và có ba ô; ở tầng type không có constructor Array(n) và không có + để tăng bộ đếm. Bạn cần một type nhận 3 và sinh tuple mà compiler nhìn thấy có đúng ba phần tử.
Cách thử ngây thơ — và vì sao thất bại
Bản năng đầu tiên có thể là “cộng” ở tầng type giống JavaScript:
// ❌ does not work — types are not values
type Naive = 2 + 3; // Error: Type '2' is not assignable to type 'number' in an expression context
Type TypeScript bị xoá lúc biên dịch; chúng không chạy như giá trị. Không có +, -, hay > cho number literal type — chỉ có phép cấu trúc trên tuple, union, và conditional. Vậy “3” không thể nghĩa là “ba thứ” cho tới khi bạn biểu diễn ba thứ bằng tuple rồi đọc ['length'].
Cơ chế — đếm bằng độ dài tuple
type BuildTuple<
L extends number,
T extends unknown[] = [],
> = T['length'] extends L ? T : BuildTuple<L, [...T, unknown]>;
type A = BuildTuple<3>; // [unknown, unknown, unknown]
type N = BuildTuple<3>['length']; // 3
Đọc từng dòng:
- độ dài đích cần đạt.
- accumulator: tuple đã dựng, bắt đầu rỗng.
- nếu accumulator đã có
Lphần tử, xong; trả về nó. - ngược lại nối một
unknownrồi đệ quy.
Đây là đệ quy đuôi (phần tăng nằm trong T, không phải return type lồng nhau), quan trọng với giới hạn độ sâu — xem Phần 5.
Đánh giá cụ thể: BuildTuple<3>
// step 0: BuildTuple<3>
// L = 3, T = []
// [].length extends 3 ? … : BuildTuple<3, [unknown]>
// step 1: BuildTuple<3, [unknown]>
// T['length'] = 1, still not 3
// → BuildTuple<3, [unknown, unknown]>
// step 2: BuildTuple<3, [unknown, unknown]>
// T['length'] = 2, still not 3
// → BuildTuple<3, [unknown, unknown, unknown]>
// step 3: BuildTuple<3, [unknown, unknown, unknown]>
// T['length'] extends 3 → true
// → result: [unknown, unknown, unknown]
type A = BuildTuple<3>; // [unknown, unknown, unknown]
type N = BuildTuple<3>['length']; // 3 — tuple length *is* the number
Một khi số đã thành tuple, trực giác tầng giá trị (push, concat, slice) ánh xạ thẳng sang phép type.
Cạm bẫy
- Độ sâu đệ quy: mỗi bước tăng là một bước đệ quy;
Lrất lớn có thể chạm giới hạn độ sâu compiler. - Chỉ số tự nhiên: độ dài tuple không bao giờ âm;
BuildTuplemô hình hoá 0, 1, 2, …. unknowntùy ý: dùngunknownlàm phần tử giữ chỗ; loại phần tử không quan trọng — chỉ số lượng.
Tóm tắt: BuildTuple<L> tăng tuple rỗng tới khi ['length'] bằng L.
Cộng — bằng nối tuple
Mục tiêu
Cài Add<A, B> để Add<2, 3> ra 5. Cộng có vẻ hiển nhiên ở runtime — nhưng type checker không có toán tử + trên literal.
type R = Add<5, 3>; // 8
Cách thử ngây thơ
// ❌ same problem as before — no arithmetic on types
type NaiveAdd<A extends number, B extends number> = A + B;
Không thể cộng number literal trong biểu thức type. Cần biểu diễn mà “gộp hai đống” thành cấu trúc đo được.
Hiện đáp án
type Add<A extends number, B extends number> = [
...BuildTuple<A>,
...BuildTuple<B>,
]['length'];Cơ chế — nối = cộng
Nếu A là tuple độ dài A và B là tuple độ dài B, trải cả hai vào một mảng cho độ dài A + B. Từng dòng:
- biến
Athành tupleAô. - tương tự cho
B. - nối; spread tuple ở tầng type là nối mảng.
- đọc độ dài gộp về lại thành type số.
Đánh giá cụ thể: Add<2, 3>
// step 1: BuildTuple<2>
// [] → [unknown] → [unknown, unknown]
// BuildTuple<2> = [unknown, unknown]
// step 2: BuildTuple<3>
// [] → [unknown] → [unknown, unknown] → [unknown, unknown, unknown]
// BuildTuple<3> = [unknown, unknown, unknown]
// step 3: concatenate
// [...BuildTuple<2>, ...BuildTuple<3>]
// = [...[unknown, unknown], ...[unknown, unknown, unknown]]
// = [unknown, unknown, unknown, unknown, unknown]
// step 4: read length
// [unknown, unknown, unknown, unknown, unknown]['length'] → 5
type Result = Add<2, 3>; // 5Nối = cộng. Đây là ý tưởng quan trọng nhất trong toán tầng type.
Cạm bẫy
- Chi phí đơn nguyên:
Add<900, 900>dựng tuple ~1800 phần tử và có thể vượt độ sâu đệ quy. - Phụ thuộc
BuildTuple: nếu toán hạng quá lớn không materialize được,Addlỗi trước khi nối. - Chỉ số nguyên không âm: cả hai toán hạng là số tự nhiên qua độ dài tuple.
Tóm tắt: Add dựng hai tuple đếm rồi đo độ dài phần nối.
Trừ — bằng khớp mẫu
Mục tiêu
Cài Subtract<A, B> để Subtract<8, 3> là 5 — “bỏ B phần tử khỏi đống A”.
type R = Subtract<8, 3>; // 5
Cách thử ngây thơ
// ❌ no subtraction operator on types
type NaiveSubtract<A extends number, B extends number> = A - B;
Dù trừ literal được, vẫn cần mô hình cho “còn lại sau khi bỏ B thứ”. Tuple cung cấp điều đó: khớp “B phần tử đầu, rồi phần còn” và đo Rest.
Hiện đáp án
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
? Rest['length']
: never;Cơ chế — khớp mẫu để tiêu thụ
Add trải để dựng, còn Subtract khớp mẫu để tiêu thụ. Từng dòng:
- đống đủ
Aphần tử. - mẫu: “tuple này bắt đầu bằng thứ độ dài
B, phần còn làRest”. - nếu khớp, đáp án là bao nhiêu phần tử không bị lấy.
- nếu
B > A, không bóc đượcBphần tử từ tuple ngắn hơn; không có kết quả số tự nhiên.
Đánh giá cụ thể: Subtract<8, 3>
// step 1: BuildTuple<8> → 8 × unknown
// step 2: BuildTuple<3> → 3 × unknown
// step 3: pattern match
// [u,u,u,u,u,u,u,u] extends [...[u,u,u], ...infer Rest] ?
// Rest is inferred as [u,u,u,u,u] (5 elements)
// step 4: Rest['length'] → 5
type Result = Subtract<8, 3>; // 5Thất bại cụ thể: Subtract<3, 5>
// BuildTuple<3> = [u, u, u] (3 elements)
// BuildTuple<5> = [u, u, u, u, u] (5 elements)
// Can [u, u, u] match [...[u,u,u,u,u], ...infer Rest] ?
// No — you cannot prefix a 3-element tuple with 5 elements.
// Conditional fails → never
type Bad = Subtract<3, 5>; // neverCạm bẫy
nevernghĩa là “không xác định trong ℕ”, không phải “âm năm” — độ dài tuple chỉ mô hình hoá 0, 1, 2, ….infer Restphải là rest spread — mẫu[BuildTuple<B>, ...infer Rest](không spread đầuB) sẽ không destructure đúng.- Cả hai bên trả chi phí
BuildTuple— trừ số lớn materialize hai tuple lớn.
Tóm tắt: trừ bằng cách khớp tuple A thành “tiền tố B + phần còn”, rồi đọc Rest['length'].
4425 · GreaterThan
Mục tiêu
Trả true khi A > B, false khi A ≤ B (kể cả bằng). Không có toán tử >, so sánh phải mã hoá bằng cấu trúc.
type A = GreaterThan<2, 1>; // true
type B = GreaterThan<1, 2>; // false
type C = GreaterThan<5, 5>; // false
Cách thử ngây thơ
// ❌ no comparison operators on number literals
type NaiveGT<A extends number, B extends number> = A > B;
Có thể thử dựng cả hai tuple rồi so length — nhưng chỉ cho biết bằng nhau, không cho thứ tự. Mẹo kinh điển: đếm lên từ 0; đích chạm trước khi đếm là số nhỏ hơn.
Hiện đáp án
type GreaterThan<
A extends number,
B extends number,
Count extends unknown[] = [],
> = A extends B
? false
: Count['length'] extends A
? false
: Count['length'] extends B
? true
: GreaterThan<A, B, [...Count, unknown]>;Cơ chế — đua hai đích
Từng dòng:
- số bằng nhau không lớn hơn hẳn.
- tuple đếm bắt đầu độ dài 0.
- nếu chạm
AtrướcB,Anhỏ hơn →A > Blà false. - nếu chạm
Btrước,Bnhỏ hơn →A > Blà true. - chưa chạm đích nào; tăng
Countrồi đệ quy.
Đánh giá cụ thể: GreaterThan<2, 1>
// step 0: Count = [], length 0
// 2 extends 1? no
// 0 extends 2? no
// 0 extends 1? no
// → recurse with Count = [unknown]
// step 1: Count = [unknown], length 1
// 1 extends 2? no
// Count['length'] extends A → 1 extends 2? no
// Count['length'] extends B → 1 extends 1? yes → true
type Result = GreaterThan<2, 1>; // true — hit B (1) before A (2)Đánh giá cụ thể: GreaterThan<1, 2>
// step 0: Count length 0 → no hits → Count = [unknown]
// step 1: Count length 1
// 1 extends 2? no
// Count['length'] extends A → 1 extends 1 → true → false
type Result = GreaterThan<1, 2>; // false — hit A (1) before B (2)Đánh giá cụ thể: GreaterThan<5, 5>
// step 0: A extends B → 5 extends 5 → true → false immediately
type Result = GreaterThan<5, 5>; // falseSố bạn chạm trước khi đếm từ 0 là số nhỏ hơn — đó là toàn bộ phép so sánh.
Cạm bẫy
- Trường hợp bằng xử lý trước — không có
A extends B ? false, số bằng sẽ rơi vào cuộc đua đếm. - Độ sâu là
min(A, B) + 1— so 500 với 499 đi ~499 bước. - Chỉ bất đẳng thức nghiêm — hòa luôn trả
false(đây là>, không phải>=).
Tóm tắt: đếm từ 0; toán hạng khớp trước là số nhỏ hơn.
4182 · Fibonacci
Mục tiêu
Tính số Fibonacci thứ N ở tầng type (dãy 1, 1, 2, 3, 5, 8, …). Khó hơn Add vì mỗi bước phụ thuộc hai giá trị trước, không phải một toán hạng cố định.
type A = Fibonacci<3>; // 2
type B = Fibonacci<8>; // 21
Cách thử ngây thơ
// ❌ cannot index into a "list of results" — types don't store computed arrays
type NaiveFib<N extends number> = [1, 1, 2, 3, 5][N];
Không có mảng runtime ở tầng type, và không đệ quy N - 1 với N - 2 nếu chưa có Subtract. Giải pháp là vòng lặp có trạng thái: mang Cur và Prev làm độ dài tuple và tiến bằng trick nối giống Add.
Hiện đáp án
type Fibonacci<
N extends number,
Cur extends unknown[] = [unknown],
Prev extends unknown[] = [],
Index extends unknown[] = [unknown],
> = Index['length'] extends N
? Cur['length']
: Fibonacci<N, [...Cur, ...Prev], Cur, [...Index, unknown]>;Cơ chế — ba accumulator
Ta mang ba tuple làm trạng thái:
| Tuple | Role at value level | Initial value |
|---|---|---|
Cur | current Fibonacci value | [unknown] → length 1 (F₁) |
Prev | previous value | [] → length 0, then updated each step |
Index | which position we’re computing | [unknown] → we’re past F₁, working toward Fₙ |
Từng dòng:
- dừng khi đã tiến tới vị trí
N. Fibonacci<N, [...Cur, ...Prev], Cur, [...Index, unknown]>— one loop iteration:Curmới = nốiCurcũ +Prev→ cộng qua spread.Prevmới =Curcũ.Indexmới =Indexcũ thêm một phần tử.
Đánh giá cụ thể: Fibonacci<3>
// Initial: Cur=[u] (len 1), Prev=[] (len 0), Index=[u] (len 1)
// Target N = 3
// ── iteration 1 ──
// Index len 1 extends 3? no
// new Cur = [...[u], ...[]] = [u] → len 1 (F₂)
// new Prev = [u] → len 1 (old Cur)
// new Index = [u,u] → len 2
// ── iteration 2 ──
// Index len 2 extends 3? no
// new Cur = [...[u], ...[u]] = [u,u] → len 2
// new Prev = [u] → len 1
// new Index = [u,u,u] → len 3
// ── iteration 3 ──
// Index len 3 extends 3? yes → return Cur['length'] = 2
type Result = Fibonacci<3>; // 2Lần tay trên giấy: F₁=1, F₂=1, F₃=2 — type trả 2.
Cạm bẫy
CurvàIndexkhởi tạo[unknown]— indexing challenge coiFibonacci<1>là1; vòng lặp căn theo base case đó.- Mỗi bước hai lần nối —
Curtăng nhanh;Fibonacci<30>đã nặng. - Độ sâu ≈ N — một đệ quy mỗi nhịp
Index;Nlớn chạm giới hạn nhanh.
Tóm tắt: Fibonacci là vòng lặp đệ quy đuôi với tuple Cur, Prev, Index; mỗi bước cộng độ dài qua nối.
Đôi lời về giới hạn
Số học tầng type là đơn nguyên (đếm bằng unknown), nên chậm và bị chặn bởi giới hạn độ sâu đệ quy (~1000 với đệ quy đuôi). Add<900, 900> có thể đã lỗi. Đây là công cụ học, không phải thứ để dùng trong type thật. Nếu thật sự cần tính số, hãy tính ở tầng giá trị lúc chạy, không phải trong type.
Bài tập
1. Cài MultiplyByTwo<N> (gợi ý: cộng N với chính nó).
Lời giải
type MultiplyByTwo<N extends number> = Add<N, N>;Nhân đôi là cộng với cùng toán hạng hai lần — tái dùng thứ đã dựng.
// step 1: BuildTuple<N> twice, concat, read length → 2 × N
type Double5 = MultiplyByTwo<5>; // 102. Vì sao Subtract<3, 5> trả never thay vì số âm?
Lời giải
Độ dài tuple mô hình hoá số tự nhiên (0, 1, 2, …) — không có tuple độ dài âm. Mẫu [...BuildTuple<5>, ...infer Rest] không khớp tuple 3 phần tử (không bỏ 5 từ 3), nên conditional rơi xuống never, báo trung thực “không có kết quả tự nhiên”.
// BuildTuple<3> has 3 slots; prefix pattern needs 5 → match fails → never
type Bad = Subtract<3, 5>; // neverNâng cao:cài Multiply<A, B> bằng cộng lặp với accumulator — và để ý bạn chạm giới hạn độ sâu nhanh thế nào.
Điểm chính
- Một số là tuple có độ dài đó;
['length']đổi ngược. - Cộng = nối; Trừ = khớp mẫu và đo phần còn lại.
- So sánh bằng đếm lên; đích chạm trước là số nhỏ hơn.
- Nhiều accumulator cho vòng lặp có trạng thái (Fibonacci).
- Nó đơn nguyên và bị chặn độ sâu — công cụ học, không phải code production.
Tiếp theo
Phần 9 — Utility Type khó: những utility type đời thực xuất hiện trong thư viện. Ta chia object theo key bắt buộc hay optional (GetRequired, RequiredKeys, OptionalKeys) và dựng một builder Chainable fluent, an toàn kiểu.