jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ó L phần tử, xong; trả về nó.
  • ngược lại nối một unknown rồ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; L rấ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; BuildTuple mô hình hoá 0, 1, 2, ….
  • unknown tùy ý: dùng unknown là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 AB là tuple độ dài B, trải cả hai vào một mảng cho độ dài A + B. Từng dòng:

  1. biến A thành tuple A ô.
  2. tương tự cho B.
  3. nối; spread tuple ở tầng type là nối mảng.
  4. đọ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>; // 5

Nố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, Add lỗ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>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:

  1. đống đủ A phần tử.
  2. mẫu: “tuple này bắt đầu bằng thứ độ dài B, phần còn là Rest”.
  3. nếu khớp, đáp án là bao nhiêu phần tử không bị lấy.
  4. nếu B > A, không bóc được B phầ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>; // 5

Thấ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>; // never

Cạm bẫy

  • never nghĩ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 Rest phải là rest spread — mẫu [BuildTuple<B>, ...infer Rest] (không spread đầu B) 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:

  1. số bằng nhau không lớn hơn hẳn.
  2. tuple đếm bắt đầu độ dài 0.
  3. nếu chạm A trước B, A nhỏ hơn → A > B là false.
  4. nếu chạm B trước, B nhỏ hơn → A > B là true.
  5. chưa chạm đích nào; tăng Count rồ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>; // false

Số 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 CurPrev 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:

TupleRole at value levelInitial value
Curcurrent Fibonacci value[unknown] → length 1 (F₁)
Prevprevious value[] → length 0, then updated each step
Indexwhich position we’re computing[unknown] → we’re past F₁, working toward Fₙ

Từng dòng:

  1. dừng khi đã tiến tới vị trí N.
  2. Fibonacci<N, [...Cur, ...Prev], Cur, [...Index, unknown]> — one loop iteration: Cur mới = nối Cur cũ + Prevcộng qua spread. Prev mới = Cur cũ. Index mới = Index cũ 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>; // 2

Lần tay trên giấy: F₁=1, F₂=1, F₃=2 — type trả 2.

Cạm bẫy

  • CurIndex khởi tạo [unknown] — indexing challenge coi Fibonacci<1>1; vòng lặp căn theo base case đó.
  • Mỗi bước hai lần nốiCur tăng nhanh; Fibonacci<30> đã nặng.
  • Độ sâu ≈ N — một đệ quy mỗi nhịp Index; N lớ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>; // 10

2. 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>; // never

Nâ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.