jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

TS Type Challenges · Part 7 — Template Literal Types

Part 7: string math at the type level. Build Trim, Replace, ReplaceAll, StartsWith/EndsWith and LengthOfString with template-literal pattern matching and recursion — each with a toggle-to-reveal answer and a step-by-step explanation.

Đây là Phần 7 của series 12 bài về TypeScript type challenges. Template literal type cho phép dựng string (`Hello, ${S}`) và, mạnh hơn, khớp mẫu chúng bằng infer. Kết hợp đệ quy, bạn xử lý string từng ký tự — nền cho mọi parser ta viết ở Phần 10.

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


Khớp tham lam chia chuỗi thế nào

Mục tiêu

Trước khi viết Trim, Replace, hay Split, bạn cần một mô hình tư duy: khi TypeScript khớp một string với mẫu template, nó quyết định mỗi ô infer bắt đầu và kết thúc ở đâu thế nào?. Hiểu sai điểm này thì mọi parser string đệ quy trông như phép màu.

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

Ở runtime, split('-') hiển nhiên — JavaScript duyệt string và cắt ở mọi dấu phân cách. Ở tầng type không có vòng lặp: compiler cố cho một mẫu khớp một lần, và quy tắc ô nào bắt ký tự nào không được ghi rõ như bạn mong.

Thử ngây thơ — và vì sao thất bại

Bạn có thể nghĩ hai ô infer liền kề chia đều string, hoặc ô thứ hai không tham lam:

// ❌ Wrong mental model: "H takes half, T takes half"
type WrongSplit = 'a-b-c' extends `${infer H}-${infer T}` ? [H, T] : never;
// You might guess: H = 'a-b', T = 'c'  — but that's NOT what happens

TypeScript không tách ở dấu - cuối. Nó cũng không tối thiểu H rồi tối đa T kiểu lazy quantifier regex. Quy tắc thật là bất đối xứng.

Cơ chế

Khi mẫu có hai ô infer liền kề quanh một đoạn literal, infer đầu không tham lam (lấy tối thiểu) và ô thứ hai lấy hết phần còn lại:

type Split = 'a-b-c' extends `${infer H}-${infer T}` ? [H, T] : never;
// H = 'a',  T = 'b-c'   (splits at the FIRST '-')

Đọc mẫu từ trái sang phải:

  1. bắt ít ký tự nhất có thể mà vẫn cho phần còn lại của mẫu khớp.
  2. khớp dấu gạch ngang literal.
  3. bắt mọi thứ còn lại.

Nên `${infer H}-${infer T}` luôn tách ở dấu phân cách đầu tiên. Để xử lý mọi dấu phân cách, đệ quy trên T.

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

Xem compiler khớp 'a-b-c' với `${infer H}-${infer T}`:

// step 0: candidate string = 'a-b-c', pattern = `${infer H}-${infer T}`

// step 1: try H = '' (empty — minimum possible)
//   remainder must match `-${infer T}` → '-b-c' ✓
//   T = 'b-c'
//   SUCCESS on first try → compiler stops (non-greedy H wins)

type Split = 'a-b-c' extends `${infer H}-${infer T}` ? [H, T] : never;
// Result: ['a', 'b-c']
// (H = 'a' because H = '' also works structurally, but TS picks the match
//  that satisfies the full conditional — here H = 'a', T = 'b-c')

Với ô thứ hai sau hậu tố literal, ngược lại — infer đầu tham lam:

// Pattern: `${infer R}${Whitespace}` — trim trailing space
// String:  'hi '

// step 1: R tries to be as LARGE as possible while still ending in Whitespace
//   R = 'hi', trailing char = ' ' ✓
//   (R = 'hi ' would fail — nothing left for Whitespace)

type TrimRightDemo = 'hi ' extends `${infer R} ` ? R : never;
// Result: 'hi'

Cạm bẫy

  • Chỉ dấu phân cách đầu — một lần extends không bao giờ duyệt hết string; đệ quy bắt buộc cho bài “mọi lần xuất hiện” như ReplaceAllSplit.
  • Tham lam vs không tham lam đổi vai — mẫu tiền tố (`${infer H}literal`) làm ô đầu tham lam; mẫu xen giữa (`${infer H}literal${infer T}`) làm ô đầu không tham lam.
  • Phân phối — nếu string bên trái là union, cả conditional phân phối (xem Phần 6); bản thân khớp template không “thử mọi điểm tách”.

Tóm lại: hai ô infer quanh literal → ô đầu tối thiểu, ô sau lấy phần còn lại; một infer trước literal đuôi → ô đó tối đa.


108 · Trim

Mục tiêu

Xoá khoảng trắng đầu cuối khỏi một string type — space, tab, newline. Input ' hello world ' phải thành 'hello world'.

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

Không có .trim() trên string type. Bạn không thể index vào string type hay cắt bằng substring. Công cụ duy nhất là khớp mẫu — bóc một ký tự trắng ở một đầu, rồi đệ quy.

type R = Trim<'  hello world  '>; // 'hello world'

Thử ngây thơ — và vì sao thất bại

// ❌ Strings have no .length or [index] at the type level
type NaiveTrim<S extends string> = S; // can't "drop first char" without a pattern

// ❌ A single pattern can't trim BOTH ends at once without careful design
type OneShot<S extends string> = S extends ` ${infer R} ` ? R : S;
// Only works when EXACTLY one space wraps the whole string — fails on
// '  hello  ' (multiple leading spaces) and '\thello\n' (other whitespace)

Bạn cần mẫu theo hướng — một hình cho đầu trái, một cho đầu phải — rồi hợp chúng.

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

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

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

type Trim<S extends string> = TrimLeft<TrimRight<S>>;

Cơ chế

Ba mảnh ghép cùng nhau:

  1. Union Whitespace — tham số type ở vị trí template khớp nếu ký tự tiếp theo là bất kỳ thành viên nào của union.
  2. TrimLeft — mẫu `${Whitespace}${infer R}` nghĩa là “bắt đầu bằng khoảng trắng; phần còn lại vào R”.
  3. TrimRight — mẫu đối xứng `${infer R}${Whitespace}`; ở đây R tham lam, lấy hết trừ ký tự trắng cuối.

Trim hợp TrimLeft<TrimRight<S>> — cắt đầu phải trước (để khoảng trắng giữa giữ nguyên), rồi duyệt đầu trái. Thứ tự ít quan trọng vì hai thao tác độc lập, nhưng TrimRight trước là lựa chọn quen từ type-challenges.

Từng dòng

type TrimLeft<S extends string> = S extends `${Whitespace}${infer R}`
  ? TrimLeft<R>   // matched → drop one leading whitespace, recurse on R
  : S;            // no leading whitespace → done
type TrimRight<S extends string> = S extends `${infer R}${Whitespace}`
  ? TrimRight<R>  // matched → drop one trailing whitespace, recurse on R
  : S;            // no trailing whitespace → done
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
// TrimRight peels '  hello world  ' → '  hello world'
// TrimLeft  peels '  hello world'   → 'hello world'

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

Theo dõi Trim<' \thello '> (tab + space đầu, space cuối):

// ── TrimRight phase ──
// step 0: S = ' \thello  '
// step 1: matches `${infer R} ` with R = ' \thello ' → TrimRight<' \thello '>
// step 2: matches `${infer R} ` with R = ' \thello'  → TrimRight<' \thello'>
// step 3: no trailing space → ' \thello'

// ── TrimLeft phase ──
// step 4: S = ' \thello'
// step 5: matches ` ${infer R}` with R = '\thello'  → TrimLeft<'\thello'>
// step 6: matches `\t${infer R}` with R = 'hello'    → TrimLeft<'hello'>
// step 7: no leading whitespace → 'hello'

type Result = Trim<' \thello  '>; // 'hello'

Cạm bẫy

  • Whitespace phải là union — viết ` ${infer R}` chỉ cắt space, không cắt \t hay \n.
  • Khoảng trắng giữa được giữ — mẫu chỉ chạm hai đầu; ' hello world ''hello world'.
  • Đệ quy vô tận? — chỉ khi đệ quy mà string không ngắn lại; mỗi lần khớp bỏ đúng một ký tự, nên chắc chắn dừng.

Tóm lại: mỗi bước đệ quy bóc một khoảng trắng ở một đầu; hợp trim trái + phải.


116 · Replace

Mục tiêu

Thay lần xuất hiện đầu tiên của substring From bằng To trong string S. Replace<'foobarbar', 'bar', 'foo'>'foofoobar' Replace<'foobarbar', 'bar', 'foo'>'foofoobar'.

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

Khác string.replace() runtime, bản tầng type phải định vị substring không có vòng lặp tìm kiếm — bạn nhúng From làm đoạn literal trong mẫu template và để compiler tìm chỗ khớp đầu tiên. Bạn cũng cần guard rõ cho From rỗng, mà API runtime âm thầm bỏ qua.

type R = Replace<'foobarbar', 'bar', 'foo'>; // 'foofoobar'
type E = Replace<'foobarbar', '', 'foo'>;    // 'foobarbar' (empty From is a no-op)

Thử ngây thơ — và vì sao thất bại

// ❌ No guard for empty From
type BadReplace<S extends string, From extends string, To extends string> =
  S extends `${infer Head}${From}${infer Tail}`
    ? `${Head}${To}${Tail}`
    : S;
// Replace<'abc', '', 'Z'> → 'Zabc' — inserts at position 0 forever if you recurse

// ❌ Trying to "find" with string indexing — doesn't exist at type level
type IndexReplace<S extends string> = S[0]; // Error: strings have no numeric index

Cách sửa là guard conditional cộng template ba phần với From kẹp giữa hai ô infer.

Hiện đáp án
type Replace<
  S extends string,
  From extends string,
  To extends string,
> = From extends ''
  ? S
  : S extends `${infer Head}${From}${infer Tail}`
    ? `${Head}${To}${Tail}`
    : S;

Cơ chế

  1. Guard rỗngFrom extends '' ? S thoát sớm trước khi khớp.
  2. Nhúng literalFrom xuất hiện nguyên văn trong mẫu, không nằm trong infer; compiler phải tìm đúng substring đó.
  3. Head không tham laminfer đầu lấy tiền tố tối thiểu trước khi From xuất hiện, tức đúng “lần đầu tiên”.
  4. Dựng lại`${Head}${To}${Tail}` chèn To thay chỗ From đã khớp.

Từng dòng

From extends '' ? S
// If there's nothing to find, return S unchanged (no-op)

: S extends `${infer Head}${From}${infer Tail}`
// Try to split S into [prefix][From][suffix]
// Head = minimal prefix, Tail = everything after first From

  ? `${Head}${To}${Tail}`
  // Found → rebuild with To substituted once

  : S;
  // From not found anywhere → return original string

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

Theo dõi Replace<'foobarbar', 'bar', 'foo'>:

// step 0: From = 'bar' (non-empty → proceed to match)
// step 1: pattern = `${infer Head}${'bar'}${infer Tail}` against 'foobarbar'
// step 2: Head minimal → Head = 'foo', literal 'bar' matches, Tail = 'bar'
// step 3: rebuild = `${'foo'}${'foo'}${'bar'}` = 'foofoobar'

type R = Replace<'foobarbar', 'bar', 'foo'>; // 'foofoobar'

Theo dõi ca From rỗng:

// step 0: From = '' → guard fires immediately → return S
type E = Replace<'foobarbar', '', 'foo'>; // 'foobarbar'

Cạm bẫy

  • Chỉ khớp đầu — không đệ quy nên 'bar' sau trong 'foobarbar' giữ nguyên sau bước 3.
  • From rỗng là tiền tố mọi vị trí — không có guard, `${infer Head}${''}${infer Tail}` khớp ở index 0 với Head = '', làm hỏng string.
  • From phải là literal cụ thểReplace<'abc', string, 'x'> không tìm “bất kỳ string”; From cố định mỗi lần instantiate.

Tóm lại: guard From rỗng, khớp `${Head}${From}${Tail}`, dựng lại một lần — không đệ quy.


119 · ReplaceAll

Mục tiêu

Thay mọi lần xuất hiện của From bằng To — không chỉ lần đầu. ReplaceAll<'t y p e s', ' ', ''>'types' ReplaceAll<'t y p e s', ' ', ''>'types'.

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

Replace đã tìm được lần đầu; bước tiếp theo tự nhiên là đệ quy. Phần tinh tế là đệ quy ở đâu — đệ quy sai đoạn thì hoặc bỏ sót khớp hoặc lặp vô tận.

type R = ReplaceAll<'t y p e s', ' ', ''>; // 'types'

Thử ngây thơ — và vì sao thất bại

// ❌ Recurse on the rebuilt string — re-scans the part you just inserted
type BadReplaceAll<S extends string, From extends string, To extends string> =
  S extends `${infer Head}${From}${infer Tail}`
    ? ReplaceAll<`${Head}${To}${Tail}`, From, To>  // re-scans Head+To
    : S;
// When To contains From (e.g. replace 'a' with 'aa'), this can loop forever

// ❌ No empty-From guard — same corruption as Replace
type BadReplaceAll2<S extends string, To extends string> =
  S extends `${infer Head}${''}${infer Tail}`
    ? `${Head}${To}${ReplaceAll<Tail, '', To>}`
    : S;

Đáp án type-challenges chỉ đệ quy trên Tail — substring sau From đã khớp, không bao giờ xem lại Head hay To vừa chèn.

Hiện đáp án
type ReplaceAll<
  S extends string,
  From extends string,
  To extends string,
> = From extends ''
  ? S
  : S extends `${infer Head}${From}${infer Tail}`
    ? `${Head}${To}${ReplaceAll<Tail, From, To>}`
    : S;

Cơ chế

Cùng guard và mẫu với Replace. Chỉ đổi lời gọi đệ quy trên Tail thay vì trả kết quả phẳng:

`${Head}${To}${ReplaceAll<Tail, From, To>}`
//  ^^^^^ rebuilt prefix (done, never re-scanned)
//        ^^ inserted once
//           ^^^^^^^^^^^^^^^^^^^^^^^^^ scan continues HERE only

Mỗi lần gọi xử lý một From ở vị trí còn lại ngoài cùng bên trái; Tail thu nhỏ hoặc vẫn khớp được tới khi hết From.

Từng dòng

? `${Head}${To}${ReplaceAll<Tail, From, To>}`
// Head and To are finalized in this branch — only Tail may still contain From
// ReplaceAll<Tail, ...> is the "loop body" moving left-to-right through S

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

Theo dõi ReplaceAll<'t y p e s', ' ', ''>:

// call 1: S = 't y p e s'
//   Head = 't', From = ' ', Tail = 'y p e s'
//   partial = 't' + '' + ReplaceAll<'y p e s', ' ', ''>

// call 2: S = 'y p e s'
//   Head = 'y', Tail = 'p e s'
//   partial = 'y' + ReplaceAll<'p e s', ' ', ''>

// call 3: S = 'p e s'  → 'p' + ReplaceAll<'e s', ' ', ''>
// call 4: S = 'e s'    → 'e' + ReplaceAll<'s', ' ', ''>
// call 5: S = 's'      → no space → 's'

// unwind: 't' + 'y' + 'p' + 'e' + 's' = 'types'

type R = ReplaceAll<'t y p e s', ' ', ''>; // 'types'

Cạm bẫy

  • Đệ quy trên Tail, không phải chuỗi dựng lại đầy đủ — tránh quét lại To và tránh nở vô tận khi To chứa From.
  • Trái sang phải, không chồng lấn — sau khi thay một From, lần tìm tiếp bắt đầu sau nó (ngữ nghĩa replaceAll chuẩn).
  • Độ sâu = số lần khớp — string rất dài với nhiều lần thay có thể chạm giới hạn độ sâu đệ quy ở bản TS cũ.

Tóm lại: Replace + ReplaceAll<Tail> — chốt phần trái, tiếp tục quét phần phải.


2688 · StartsWith & 2693 · EndsWith

Mục tiêu

Trả true hoặc false: string T có bắt đầu bằng substring U không? Có kết thúc bằng U không?.

type A = StartsWith<'abc', 'ab'>; // true
type B = EndsWith<'abc', 'bc'>;   // true

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

Bản năng đầu có thể là đệ quy — bóc từng ký tự rồi so. Cách đó được, nhưng template literal có mẫu một phát dùng string làm wildcard — không đệ quy, không cần cầu tuple.

Thử ngây thơ — và vì sao thất bại

// ❌ Over-engineered recursion (works, but misses the point of this lesson)
type NaiveStartsWith<T extends string, U extends string> =
  U extends ''
    ? true
    : T extends `${infer H}${infer _}`
      ? H extends U ? true : NaiveStartsWith<_, U>  // wrong — compares one char only
      : false;

// ❌ Using `infer` where `string` wildcard is enough
type Overkill<T extends string, U extends string> =
  T extends `${U}${infer _Rest}` ? true : false;
// Works, but `_Rest` is unnecessary noise — `string` expresses "anything" directly

Đáp án thành thạo dùng string wildcard sẵn có trong mẫu template.

Hiện đáp án
type StartsWith<T extends string, U extends string> = T extends `${U}${string}`
  ? true
  : false;

type EndsWith<T extends string, U extends string> = T extends `${string}${U}`
  ? true
  : false;

Cơ chế — string như ký tự đại diện

Trong mẫu template literal (vế phải của extends), type string đặc biệt: khớp bất kỳ dãy ký tự nào (kể cả rỗng). Tương đương regex .* ở tầng type.

  • `${U}${string}` — “T phải bắt đầu bằng literal U, rồi gì cũng được (hoặc không gì)”.
  • `${string}${U}` — “gì cũng được (hoặc không gì), rồi kết thúc bằng literal U”.

Không cần infer — bạn không trích phần nào, chỉ kiểm tra hình dạng.

Từng dòng

type StartsWith<T extends string, U extends string> = T extends `${U}${string}`
  ? true    // T fits the "U + anything" shape
  : false;  // no fit → prefix test fails

type EndsWith<T extends string, U extends string> = T extends `${string}${U}`
  ? true    // T fits the "anything + U" shape
  : false;

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

StartsWith<'abc', 'ab'>:

// step 0: does 'abc' extend `${'ab'}${string}`?
// step 1: literal 'ab' matches first two chars → remainder 'c' is assignable to string ✓
// step 2: conditional → true

type A = StartsWith<'abc', 'ab'>; // true

StartsWith<'abc', 'b'>:

// step 0: does 'abc' extend `${'b'}${string}`?
// step 1: first char is 'a', not 'b' → pattern fails
// step 2: conditional → false

type Fail = StartsWith<'abc', 'b'>; // false

EndsWith<'abc', 'bc'>:

// step 0: does 'abc' extend `${string}${'bc'}`?
// step 1: wildcard string absorbs 'a', literal 'bc' matches last two chars ✓
// step 2: true

type B = EndsWith<'abc', 'bc'>; // true

Cạm bẫy

  • Wildcard string chỉ hoạt động trong mẫu — ở vế trái extends, string chỉ là type rộng, không phải bộ khớp.
  • U rỗngStartsWith<'abc', ''>true`${''}${string}` khớp mọi string.
  • Phân phối — nếu T là union, conditional phân phối: StartsWith<'abc' | 'ab', 'ab'>true | truetrue.

Tóm lại: `${U}${string}` / `${string}${U}` — kiểm tra tiền tố/hậu tố một dòng, không đệ quy.


298 · Length of String

Mục tiêu

Tính độ dài string type thành number literalLengthOfString<'hello'>5.

type R = LengthOfString<'hello'>; // 5

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

Tuple có ['length']; string thì không. Không có S.length ở tầng type. Bạn phải chuyển string sang cấu trúc dữ liệu mang thông tin độ dài — tuple các string một ký tự.

Thử ngây thơ — và vì sao thất bại

// ❌ No .length on string types
type BadLen<S extends string> = S['length']; // never resolves to a number literal

// ❌ Building a union of chars loses order AND has no length
type CharUnion<S extends string> = S extends `${infer C}${infer Rest}`
  ? C | CharUnion<Rest>
  : never;
// CharUnion<'hi'> = 'h' | 'i' — union length ≠ string length

Tuple giữ thứ tự và lộ độ dài dưới dạng number literal qua ['length'].

Hiện đáp án
type StringToTuple<S extends string> = S extends `${infer Head}${infer Tail}`
  ? [Head, ...StringToTuple<Tail>]
  : [];

type LengthOfString<S extends string> = StringToTuple<S>['length'];

Cơ chế — bắc cầu sang tuple, rồi đếm

Pipeline hai bước:

  1. StringToTuple — đệ quy với `${infer Head}${infer Tail}`, prepend mỗi Head (một ký tự) vào tuple đang lớn dần.
  2. LengthOfString — đọc StringToTuple<S>['length'], cùng mẹo độ dài tuple từ Phần 1.

Đây là phiên bản string của “chuyển sang tuple rồi đo” — cùng mẫu cầu như xử lý union/tuple ở Phần 6, nhưng dựng tuple thay vì union vì thứ tự quan trọng.

Từng dòng

type StringToTuple<S extends string> = S extends `${infer Head}${infer Tail}`
  ? [Head, ...StringToTuple<Tail>]  // prepend char, recurse on rest
  : [];                              // empty string → empty tuple

type LengthOfString<S extends string> = StringToTuple<S>['length'];
// Tuple ['h','e','l','l','o'] has length 5 → literal type 5

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

Theo dõi LengthOfString<'hi'>:

// call 1: S = 'hi'
//   Head = 'h', Tail = 'i'  → ['h', ...StringToTuple<'i'>]

// call 2: S = 'i'
//   Head = 'i', Tail = ''   → ['i', ...StringToTuple<''>]

// call 3: S = ''
//   → []

// unwind: StringToTuple<'hi'> = ['h', 'i']
// ['h', 'i']['length'] = 2

type R = LengthOfString<'hi'>; // 2

Theo dõi LengthOfString<'hello'>:

// StringToTuple<'hello'> = ['h', 'e', 'l', 'l', 'o']
// length = 5

type R = LengthOfString<'hello'>; // 5

Cạm bẫy

  • Head là một ký tự, không phải một byte — string type TypeScript là đơn vị mã UTF-16; grapheme phức tạp có thể đếm thành nhiều phần tử tuple.
  • Độ sâu đệ quy = độ dài string — literal type 1000 ký tự nghĩa là ~1000 lần mở rộng đệ quy.
  • Chuỗi rỗngStringToTuple<''>[], và [ ]['length']0.

Tóm lại: parse string thành ['h','e',...] bằng đệ quy template, rồi đọc ['length'].


Bài tập

1. Cài Trim không cần TrimRight riêng, bằng cách đệ quy cả hai đầu trong một type.

Lời giải
type Whitespace = ' ' | '\n' | '\t';

type Trim<S extends string> = S extends
  | `${Whitespace}${infer R}`
  | `${infer R}${Whitespace}`
  ? Trim<R>
  : S;

Một mẫu union trong mệnh đề extends khớp nếu một trong hai hình dạng vừa. Bên nào có khoảng trắng sẽ bị cắt, và ta đệ quy tới khi không bên nào khớp. Một type xử lý cả hai đầu — ít tên hơn, hành vi giống nhau.

Từng bước

// Trim<' hi '>
// step 1: matches `${infer R}${Whitespace}` → R = ' hi' → Trim<' hi'>
// step 2: matches `${Whitespace}${infer R}` → R = 'hi'  → Trim<'hi'>
// step 3: no match → 'hi'

2. Vì sao Replace chặn From extends '' trước khi khớp?

Lời giải

Chuỗi rỗng là tiền tố của mọi vị trí. Không có guard, `${infer Head}${''}${infer Tail}` khớp ngay đầu với Head = ''Tail = S, chèn To ở vị trí 0 và làm hỏng kết quả. Trong ReplaceAll, điều đó sẽ đệ quy vô tận, chèn To lặp lại. Guard biến “thay không có gì” thành no-op đúng đắn.

Nâng cao:cài Split<S, Sep> trả tuple các đoạn, xử lý ca separator rỗng và đoạn cuối.


Điểm chính

  • `${infer H}${sep}${infer T}` tách ở sep đầu tiên; đệ quy trên T để xử lý hết.
  • Một tham số type có thể xuất hiện nguyên văn trong mẫu như đoạn cố định để khớp.
  • string là ký tự đại diện tầng type — `${U}${string}` / `${string}${U}` kiểm tra tiền tố/hậu tố.
  • Với ReplaceAll, đệ quy trên đuôi sau match để tránh quét lại và vòng lặp vô tận.
  • Để đếm ký tự, bắc cầu string sang tuple rồi đọc ['length'].

Tiếp theo

Phần 8 — Số học tầng type: TypeScript không có + cho số, nên ta đếm bằng độ dài tuple. Ta dựng Add, Subtract, GreaterThan, và cả Fibonacci, dùng các mẹo accumulator và độ dài tuple bạn đã gom được.