jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

TS Type Challenges · Part 4 — infer Mastery

Part 4: capture pieces of a type with infer. Rebuild Parameters, Last and Pop, append arguments to functions, and learn constrained infer (infer N extends number) to parse numbers from strings — each with a toggle-to-reveal answer.

Đây là Phần 4 của series 12 bài về TypeScript type challenges. infer là công cụ bắt một mảnh của type trong khi bạn khớp mẫu hình dạng của nó. Nếu conditional type là câu hỏi (“T có giống thế này không?”), thì infer là cách bạn giữ câu trả lời (“…và đưa tôi phần đó”).

Ghi nguồn: các đáp án theo type-challenges (giấy phép MIT); số khớp ID chính thức.


Mô hình tư duy

infer dùng để làm gì

Bạn thường biết hình dạng tổng thể của một type nhưng cần một mảnh — return type của hàm, phần tử cuối của tuple, substring bên trong template literal. Bạn không thể “index” vào function type bằng T[0] như với tuple. infer là cách compiler destructure type — bóc phần bạn cần trong khi khớp phần còn lại.

Cách thử ngây thơ

Giả sử bạn muốn type đã resolve bên trong Promise<number>:

// ❌ You cannot "reach inside" a generic with indexed access
type Unwrap<T> = T extends Promise<any> ? T[0] : T;
//                                      ▲ error: T is not a tuple

Hoặc bạn thử tái dùng giá trị đã có:

// ❌ `any` in the pattern does not become a named variable
type Unwrap<T> = T extends Promise<any> ? any : T;
//                                       ▲ you lose the specific type — it's just `any`

Cả hai đều thất bại vì conditional type chỉ cho có/không — bản thân nó không đặt tên cho mảnh đã khớp.

Cơ chế

infer X chỉ được xuất hiện bên trong mệnh đề extends của conditional type. Nó nói “khớp hình dạng này, và gán bất kỳ thứ gì nằm ở ô này vào biến mới X, dùng được ở nhánh true”. Hãy nghĩ là destructuring cho type.

type Unwrap<T> = T extends Promise<infer V> ? V : T;
//                                  ▲ capture the resolved type

Bạn đặt inferbất kỳ ô nào: phần tử mảng, vị trí tuple, tham số/return của hàm, đối số generic, kể cả phần con của template literal.

Từng dòng

type Unwrap<T> = T extends Promise<infer V> ? V : T;
//    ①         ②   ③        ④        ⑤    ⑥  ⑦
  1. một alias generic nhận type T bất kỳ.
  2. bắt đầu conditional: “T có khớp mẫu này không?”.
  3. mẫu: Promise mà đối số type ta bắt thành V.
  4. giới thiệu biến type mới; compiler điền bằng thứ đã nằm ở ô đó.
  5. nếu khớp thành công, kết quả là V (type bên trong).
  6. nếu khớp thất bại, trả T nguyên vẹn.

Đánh giá type từng bước

Theo Unwrap<Promise<string>> qua compiler:

// step 1: substitute T = Promise<string>
type Unwrap<T> = T extends Promise<infer V> ? V : T;

// step 2: check — does Promise<string> extend Promise<infer V>?
//         Yes. The compiler binds V = string.

// step 3: take the true branch → V → string
type Result = string;

Và khi input không phải Promise:

// step 1: substitute T = number
// step 2: does number extend Promise<infer V>? No.
// step 3: take the false branch → T → number
type Result = number;

Cạm bẫy

  • infer chỉ gán trong mệnh đề extends — không viết T extends Foo ? infer X : never; việc giới thiệu và khớp xảy ra cùng lúc.
  • Nhiều tên infer trong một mẫu là được — compiler gán tất cả cùng lúc (dùng ở AppendArgument).
  • Vị trí covariant vs contravariant quan trọng ở bài nâng cao: infer từ tham số có thể ra union; infer từ return có thể ra intersection. Phần này giữ các trường hợp covariant đơn giản.
  • infer ở nhánh thất bại là never — mẫu không khớp thì biến infer không được gán và nhánh false chạy.

Tóm lại: infer biến conditional type từ cổng kiểm tra thành bộ khớp mẫu có đặt tên cho thứ tìm được.


3312 · Parameters

Bắt danh sách tham số của hàm dưới dạng tuple. Parameters<T> built-in làm đúng việc này; bài tập là dựng lại bằng infer.

type R = MyParameters<(a: number, b: string) => void>;
// Expected: [a: number, b: string]

Vì sao không hiển nhiên

Function type không phải tuple — (a: number, b: string) => void là một callable type, không phải [number, string]. Bạn phải khớp hình dạng hàm rồi rút danh sách tham số ra thành type riêng.

Cách thử ngây thơ

// ❌ T is a function, not a tuple — no numeric index
type MyParameters<T> = T extends (...args: any[]) => any ? T[0] : never;

// ❌ `any` matches but does not capture the specific parameter tuple
type MyParameters<T> = T extends (...args: any[]) => any ? any[] : never;

Cái đầu lỗi type; cái sau “chạy” nhưng xóa mất tuple chính xác [a: number, b: string] bạn cần.

Hiện đáp án
type MyParameters<T extends (...args: any[]) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

Cơ chế

Ta khớp T với mẫu hàm và đặt infer Pô rest-parameter. Bất kỳ thứ gì chiếm ...args trong hàm thật trở thành tuple type P.

Từng dòng

type MyParameters<T extends (...args: any[]) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;
  1. ràng buộc: chỉ callable type mới vào utility này.
  2. conditional thứ hai: khớp hình dạng T; bắt cả tuple tham số thành P.
  3. bỏ qua return; any khớp mọi thứ.
  4. thành công: đáp án là tuple tham số đã bắt.
  5. không nên xảy ra với ràng buộc, nhưng thỏa hình dạng conditional.

Đánh giá type từng bước

type Fn = (a: number, b: string) => void;
type R = MyParameters<Fn>;

// step 1: T = (a: number, b: string) => void
//         constraint passes — it is a function type

// step 2: does (a: number, b: string) => void extend (...args: infer P) => any?
//         Yes. The compiler binds P = [a: number, b: string]

// step 3: true branch → P
type R = [a: number, b: string];

So với MyReturnTypePhần 1: cùng mẫu, infer chỉ chuyển từ ô return sang ô tham số.

// ReturnType pattern — infer in the RETURN slot
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

// Parameters pattern — infer in the ARGS slot (this challenge)
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

Cạm bẫy

  • Ô rest vs từng tham số...args: infer P bắt cả danh sách thành một tuple, kể cả label và optional.
  • Hàm overloadParameters (và bản này) resolve theo overload cuối trong quy tắc inference của TypeScript.
  • Ràng buộc ngoài rất quan trọng — không có T extends (...args: any[]) => any, truyền non-function sẽ rơi vào hành vi lạ.

Tóm lại: Đặt infer chỗ tuple tham số nằm — vị trí ...args — compiler trả lại thành type có tên.


15 · Last of Array

Lấy type phần tử cuối. Ở Phần 1 ta bóc từ đầu bằng First; ở đây bóc từ cuối.

type R = Last<[3, 2, 1]>; // 1
type E = Last<[]>;        // never

Vì sao không hiển nhiên

Tuple không có T[length - 1] ở tầng type. Không index ngược bằng số đã tính. Cần mẫu tuple nói “mọi thứ trước phần tử cuối, rồi phần tử cuối”.

Cách thử ngây thơ

// ❌ T[2] only works for a fixed length — not generic
type Last<T extends any[]> = T[2];

// ❌ keyof gives string keys "0" | "1" | … — awkward and not "last"
type Last<T extends any[]> = T[keyof T];

Cả hai đều trượt: cần lời giải cho mọi độ dài tuple, kể cả rỗng.

Hiện đáp án
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

Cơ chế — rest đứng đầu trong mẫu tuple

Trong First ta khớp [infer F, ...any[]] — bắt đầu, bỏ đuôi. Ở đây ta lật lại: [...any[], infer L] nghĩa là “bao nhiêu phần tử cũng được, rồi bắt phần tử cuối vào L”.

TypeScript cho phép rest ở đầu mẫu tuple chính là để bóc từ phía cuối.

Từng dòng

type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
  1. T phải là array/tuple.
  2. mẫu: không hoặc nhiều phần tử bất kỳ, rồi một phần tử cuối bắt thành L.
  3. ô “mọi thứ trước cuối”; ta bỏ.
  4. ô cuối; compiler gán type phần tử cuối.
  5. thành công trả type cuối; tuple rỗng không khớp → never.

Đánh giá type từng bước

type R = Last<[3, 2, 1]>;

// step 1: T = [3, 2, 1]
// step 2: does [3, 2, 1] extend [...any[], infer L]?
//         Match: ...any[] absorbs [3, 2], infer L binds L = 1
// step 3: true branch → L → 1
type R = 1;

Tuple rỗng:

type E = Last<[]>;

// step 1: T = []
// step 2: does [] extend [...any[], infer L]?
//         No — there is no final element to capture
// step 3: false branch → never
type E = never;

Một phần tử:

type R = Last<['only']>;

// step 2: ...any[] absorbs [] (zero elements), infer L binds L = 'only'
type R = 'only';

Cạm bẫy

  • Rest đầu là đối xứng rest cuối[infer H, ...any[]] cho đầu, [...any[], infer T] cho đuối.
  • Tuple độ dài 1 vẫn khớp — rest đầu có thể rỗng.
  • Đừng nhầm với T[number] — nó cho union mọi phần tử (3 | 2 | 1), không phải phần tử cuối.

Tóm lại: Lật mẫu First...any[] đầu nuốt prefix, infer L cuối bắt ô cuối của suffix.


16 · Pop

Trả tuple bỏ phần tử cuối. Last bắt đuôi; Pop bắt mọi thứ trừ đuôi.

type R = Pop<[3, 2, 1]>; // [3, 2]

Vì sao không hiển nhiên

Bạn cần tuple prefix, không phải một type đơn. infer phải nằm phía rest còn vị trí cuối bị bỏ.

Cách thử ngây thơ

// ❌ Captures the last element — that's Last, not Pop
type Pop<T extends any[]> = T extends [...any[], infer L] ? L : [];

// ❌ No way to "slice" without a pattern
type Pop<T extends any[]> = T extends any[] ? T : [];

Cái đầu trả 1 thay vì [3, 2]; cái sau trả nguyên cả tuple.

Hiện đáp án
type Pop<T extends any[]> = T extends [...infer Rest, any] ? Rest : [];

Cơ chế

Giờ infer nằm ở rest còn ô cuối bị bỏ bằng any trần. Vậy Rest bắt mọi thứ trừ phần tử cuối.

Từng dòng

type Pop<T extends any[]> = T extends [...infer Rest, any] ? Rest : [];
  1. bắt tất cả trừ cuối vào Rest; phần cuối phải tồn tại và khớp any.
  2. tuple prefix ta muốn trả.
  3. không infer — ta vứt type cụ thể của phần tử cuối.
  4. tuple rỗng không khớp (không có ô cuối) → trả [], không phải never, vì kết quả vẫn là tuple.

Đánh giá type từng bước

type R = Pop<[3, 2, 1]>;

// step 1: T = [3, 2, 1]
// step 2: does [3, 2, 1] extend [...infer Rest, any]?
//         Rest = [3, 2], last element 1 matches `any`
// step 3: true branch → Rest → [3, 2]
type R = [3, 2];
type R = Pop<['solo']>;

// step 2: Rest = [], last element 'solo' matches `any`
type R = [];
type R = Pop<[]>;

// step 2: pattern requires at least one trailing element — fails
type R = [];

Cạm bẫy

  • Đối xứng với ShiftShift dùng [any, ...infer Rest] bỏ phần tử đầu; cùng mẹo, đầu đối diện.
  • never vs [] khi thất bạiLast trả never (không có giá trị); Pop trả [] (tuple rỗng vẫn là kết quả tuple hợp lệ).
  • Bốn thao tác bóc tupleFirst, Last, Pop, Shift cùng họ mẫu tuple.

Đối xứng, giờ bạn viết được cả Shift ([any, ...infer Rest]) — bốn cái này (First, Last, Pop, Shift) đều là cùng một mẹo mẫu tuple.

Tóm lại: Đổi ô nào có infer — bắt rest prefix, bỏ ô cuối bằng any trần.


191 · Append Argument

Cho một type hàm và một type mới, tạo hàm có tham số đó được nối thêm. Đây là bắt-rồi-dựng-lại — công thức cốt lõi cho thao tác hàm ở tầng type.

type Fn = (a: number, b: string) => number;
type R = AppendArgument<Fn, boolean>;
// Expected: (a: number, b: string, x: boolean) => number

Vì sao không hiển nhiên

Bạn phải đọc hai mảnh từ hàm gốc (tham số và return) rồi tổng hợp hàm mới. Một infer không đủ; tuple spread từ Phần 1 (Concat) nối tham số cũ với tham số mới.

Cách thử ngây thơ

// ❌ Guesses arity — only works for this exact function
type AppendArgument<Fn, A> = (a: number, b: string, x: A) => number;

// ❌ Captures params but hard-codes return as `any`
type AppendArgument<Fn extends (...args: any[]) => any, A> =
  Fn extends (...args: infer P) => any ? (...args: [...P, A]) => any : never;

Cái đầu không generic; cái sau mất return type chính xác.

Hiện đáp án
type AppendArgument<Fn extends (...args: any[]) => any, A> = Fn extends (
  ...args: infer P
) => infer R
  ? (...args: [...P, A]) => R
  : never;

Cơ chế

Hai infer trong một lần khớp mẫu — compiler gán cả P (tuple tham số) và R (return) cùng lúc. Rồi dựng lại: spread tham số cũ, nối A, giữ R.

Từng dòng

type AppendArgument<Fn extends (...args: any[]) => any, A> = Fn extends (
  ...args: infer P
) => infer R
  ? (...args: [...P, A]) => R
  : never;
  1. Fn phải gọi được.
  2. bắt tham số hiện có thành tuple P.
  3. bắt return thành R.
  4. hàm mới: spread tham số cũ, rồi A, cùng return.
  5. variadic tuple spread từ Concat (Phần 1).

Đánh giá type từng bước

type Fn = (a: number, b: string) => number;
type R = AppendArgument<Fn, boolean>;

// step 1: Fn = (a: number, b: string) => number, A = boolean

// step 2: does Fn extend (...args: infer P) => infer R?
//         P = [a: number, b: string]
//         R = number

// step 3: rebuild (...args: [...P, A]) => R
//         [...P, A] = [...[a: number, b: string], boolean]
//                 = [a: number, b: string, boolean]

// step 4: result
type R = (a: number, b: string, boolean) => number;
// Note: TypeScript may display the third param without a name — that's cosmetic

Cạm bẫy

  • Nhiều infer trong một mẫu — mọi gán xảy ra trong một lần khớp; thứ tự infer P vs infer R không quan trọng.
  • Label tham số được giữ qua [...P, A](a: number, b: string) vẫn có label.
  • Mẫu này mở rộng được — prepend ([A, ...P]), đổi return, curry — đều là bắt-rồi-dựng-lại.

Bài này gộp bắt bằng infer với variadic tuple spread từ Concat (Phần 1) — bắt rồi dựng lại là cốt lõi của hầu hết bài thao tác hàm.

Tóm lại: infer đôi để đọc hàm, tuple spread để viết hàm mới.


infer có ràng buộc — bóc số từ string

Đây là công cụ mới, sắc hơn: bạn có thể đặt ràng buộc lên infer để việc bắt chỉ thành công (và được thu hẹp sẵn) khi khớp một type. Dùng kinh điển là bóc một số từ string literal.

type ToNumber<S extends string> = /* turn "42" into the number 42 */;
type A = ToNumber<'42'>; // 42  (a number literal, not a string)

Vì sao không hiển nhiên

infer N trong template literal thường bắt stringToNumber<'42'> với infer N thuần sẽ cho N = '42' (vẫn là string literal), không phải số 42. Bạn cần compiler kiểm tra và ép kiểu trong một bước.

Cách thử ngây thơ

// ❌ Captures the string '42', not the number 42
type ToNumber<S extends string> = S extends `${infer N}` ? N : never;
// ToNumber<'42'> → '42' (string literal)

// ❌ Pre-TS 4.8: character-counting tables — works but painful
type ToNumber<S extends string> = S extends '0' ? 0
  : S extends '1' ? 1
  : /* … hundreds of lines … */
  : never;

Cái đầu giữ sai loại (string vs number); cái sau là địa ngục bảo trì.

Hiện đáp án
type ToNumber<S extends string> = S extends `${infer N extends number}`
  ? N
  : never;

Cơ chế — infer N extends number

Trước TypeScript 4.8 bạn phải đếm ký tự để parse số ở tầng type. Giờ ${infer N extends number} làm hai việc cùng lúc:

  1. Khớp cả string như mẫu template literal rồi bắt substring.
  2. Ràng buộc phần bắt: N extends number phải đúng, ép string literal số sang type number tương ứng.

Từng dòng

type ToNumber<S extends string> = S extends `${infer N extends number}`
  ? N
  : never;
  1. input là string literal type.
  2. mẫu: cả string phải infer được thành literal number.
  3. bắt N, nhưng chỉ khi text bắt được là literal số hợp lệ thu hẹp thành number.
  4. thành công: N42, không phải '42'.
  5. string không phải số thì ràng buộc thất bại.

Đánh giá type từng bước

type A = ToNumber<'42'>;

// step 1: S = '42'
// step 2: does '42' extend `${infer N extends number}`?
//         Capture N from the whole string '42'
//         Check: does 42 (as a number literal type) extend number? Yes.
//         N is narrowed to the number literal 42
// step 3: true branch → 42
type A = 42;

Input không hợp lệ:

type B = ToNumber<'abc'>;

// step 2: capture 'abc' as N, then check N extends number — fails
// step 3: false branch → never
type B = never;

Trường hợp biên:

type C = ToNumber<'3.14'>;
// step 2: '3.14' is a valid numeric literal in TS — N = 3.14
type C = 3.14;

Cạm bẫy

  • String literal vs number literalinfer N thuần giữ '42'; infer N extends number có ràng buộc cho 42.
  • Ràng buộc có thể thất bại âm thầm — input không hợp lệ không báo lỗi; resolve thành never.
  • Cùng mẫu cho primitive khác`${infer B extends boolean}`, `${infer I extends bigint}` nối thế giới string và giá trị.
  • Nền cho Phần 8 — số học tầng type parse chữ số bằng mẹo này.

Nếu string không phải số hợp lệ ('abc'), ràng buộc thất bại, rơi xuống never. Cùng mẫu ép sang bigint, boolean,… — đó là cây cầu giữa thế giới string và số mà ta sẽ dựa nhiều ở Phần 8 (số học tầng type).

Tóm lại: infer có ràng buộc bắt và ép kiểu trong một lần khớp template literal.


106 · Trim Left — bắt bên trong template literal

infer bên trong template literal bắt substring. Cắt khoảng trắng đầu của một string type. Đây giới thiệu đệ quy ở tầng type — cùng động cơ với bài tuple, nhưng trên string.

type R = TrimLeft<'  hello'>; // 'hello'

Vì sao không hiển nhiên

String type không có .trim() lúc compile. Bạn phải bóc từng ký tự đầu bằng mẫu template, rồi đệ quy đến khi mẫu không khớp.

Cách thử ngây thơ

// ❌ Only removes exactly one space — not tabs, newlines, or multiple spaces
type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? Rest : S;

// ❌ Tries to recurse but only handles a single space character
type TrimLeft<S extends string> = S extends ` ${infer Rest}`
  ? TrimLeft<Rest>
  : S;
// TrimLeft<'\n\thello'> → '\n\thello' (unchanged)

Cả hai đều thất bại với \n, \t, hoặc nhiều khoảng trắng đầu.

Hiện đáp án
type Whitespace = ' ' | '\n' | '\t';

type TrimLeft<S extends string> = S extends `${Whitespace}${infer Rest}`
  ? TrimLeft<Rest>
  : S;

Cơ chế

Mẫu `${Whitespace}${infer Rest}` hỏi: “S có bắt đầu bằng bất kỳ ký tự trắng nào, theo sau là Rest không?”. Nếu có, đệ quy trên Rest; nếu không, trả S (base case).

Từng dòng

type Whitespace = ' ' | '\n' | '\t';

type TrimLeft<S extends string> = S extends `${Whitespace}${infer Rest}`
  ? TrimLeft<Rest>
  : S;
  1. union ba ký tự ta coi là “rác đầu”.
  2. đoạn đầu phải là một trong các ký tự đó.
  3. bắt mọi thứ sau ký tự đầu.
  4. đệ quy: thử cắt tiếp từ string ngắn hơn.
  5. base case: ký tự đầu không phải trắng → xong.

Đánh giá type từng bước

type R = TrimLeft<'  hello'>;

// ── call 1: TrimLeft<'  hello'>
// step 1: S = '  hello'
// step 2: does '  hello' extend `${Whitespace}${infer Rest}`?
//         Whitespace matches ' ' (first char)
//         Rest = ' hello'
// step 3: recurse → TrimLeft<' hello'>

// ── call 2: TrimLeft<' hello'>
// step 2: Whitespace matches ' ', Rest = 'hello'
// step 3: recurse → TrimLeft<'hello'>

// ── call 3: TrimLeft<'hello'>
// step 2: 'h' is not in Whitespace — pattern fails
// step 3: base case → 'hello'

type R = 'hello';

Khoảng trắng hỗn hợp:

type R = TrimLeft<'\n\thello'>;

// call 1: '\n' matches Whitespace, Rest = '\thello' → recurse
// call 2: '\t' matches Whitespace, Rest = 'hello' → recurse
// call 3: 'h' fails → 'hello'
type R = 'hello';

Cạm bẫy

  • Union trong template`${Whitespace}` thành công nếu ký tự đầu là bất kỳ thành viên union nào.
  • Độ sâu đệ quy — khoảng trắng đầu quá dài có thể chạm giới hạn đệ quy của TypeScript (ta xử lý đếm an toàn ở Phần 5).
  • TrimRight là đối xứng — đặt infer Rest trước whitespace: S extends `${infer Rest}${Whitespace}`.
  • Ghép TrimRight thành Trim — ta đào sâu string type ở Phần 7.

Whitespaceunion rất quan trọng: mẫu khớp khi ký tự đầu là bất kỳ một trong ba. Ghép với TrimRight là ra Trim — ta sẽ đào sâu string type ở Phần 7.

Tóm lại: infer trong template literal + đệ quy bóc từng ký tự đầu đến khi mẫu gãy.


Bài tập

1. Cài Shift<T> (bỏ phần tử đầu) và Unshift<T, E> (chèn E lên đầu).

Lời giải
type Shift<T extends any[]> = T extends [any, ...infer Rest] ? Rest : [];
type Unshift<T extends any[], E> = [E, ...T];

Shift đối xứng Pop nhưng từ đầu: [any, ...infer Rest] bỏ đầu bằng any trần, bắt đuôi thành Rest. Unshift không cần infer — chỉ là tuple spread, giống [E, ...T] bạn viết ở tầng giá trị.

2. Vì sao infer phải nằm trong mệnh đề extends chứ không phải bên phải dấu ??

Lời giải

infer giới thiệu một biến type bằng cách khớp mẫu type đang kiểm tra. Việc khớp chỉ xảy ra ở mệnh đề extends; tới nhánh true thì biến đã được gán và sẵn sàng dùng. Đặt infer sau ? thì chẳng có gì để khớp — compiler không biết destructure hình dạng nào.

// ✅ infer participates in the pattern match
type Ok<T> = T extends Promise<infer V> ? V : T;

// ❌ syntax error — infer is not allowed here
type Bad<T> = T extends Promise<any> ? infer V : T;

Coi extendsvế trái của destructuring typeinfer phải nằm chỗ có mẫu.

Nâng cao:cài ReplaceFirst<T, S> thay phần tử đầu của tuple T bằng S dùng [any, ...infer Rest] và spread.


Điểm chính

  • infer X sống trong mệnh đề extends và bắt thứ nằm ở ô của nó.
  • Đặt nó ở bất kỳ ô nào: tham số, return, vị trí mảng/tuple, generic, substring template.
  • Rest đứng đầu [...infer Rest, X] bóc từ cuối; nhiều infer trong một mẫu là bình thường.
  • infer có ràng buộc (infer N extends number) vừa bắt vừa ép — cây cầu từ string sang số.
  • Bắt-rồi-dựng-lại (với tuple spread) là công thức cho thao tác hàm/tuple.

Tiếp theo

Phần 5 — Đệ quy trên tuple: giờ đã bóc được một phần tử ở hai đầu, ta lặp. Ta dựng Reverse, Flatten, Includes, IndexOf, Join, Slice, và học cách đếm an toàn khi đệ quy mà không vượt giới hạn độ sâu.