TypeScript Type Challenges — Explained, with Toggle-to-Reveal Answers
A bilingual walkthrough of classic type-challenges (easy → hard): Pick, Readonly, Exclude, Awaited, Omit, DeepReadonly, Permutation, Union to Intersection, CamelCase — each with a toggle-to-reveal answer and a detailed explanation.
Type-Level Programming, One Challenge at a Time {Lập trình ở tầng type, từng bài một}
TypeScript’s type system is Turing-complete {Hệ thống type của TypeScript Turing-complete} — you can compute with types the way you compute with values {bạn có thể tính toán bằng type như tính bằng giá trị}. The famous type-challenges repo (MIT) is the best gym for this skill {Repo nổi tiếng type-challenges (MIT) là phòng gym tốt nhất cho kỹ năng này}.
This post walks through a curated set from easy to hard {Bài này đi qua một tập chọn lọc từ dễ tới khó}. Each challenge shows the goal first {Mỗi bài hiện mục tiêu trước}; the answer is hidden behind a toggle so you can try it yourself, then reveal and read the explanation {đáp án được giấu sau một nút gạt để bạn tự thử, rồi mở ra đọc giải thích}. The explanations are written so that even if a concept is new to you, you’ll understand it by the end {Phần giải thích được viết sao cho dù khái niệm còn mới với bạn, đọc xong bạn sẽ hiểu}.
Attribution {Ghi nguồn}: the solutions below follow the canonical community approaches from the type-challenges project (MIT licensed). Numbers match the official challenge IDs {các đáp án dưới đây theo cách giải chuẩn của cộng đồng type-challenges (giấy phép MIT); số thứ tự khớp ID chính thức}.
All challenges assume strict mode is on {Mọi bài giả định bật strict mode}.
Five Building Blocks You Need First {Năm khối nền tảng cần nắm trước}
Before the challenges, let’s understand the five tools that make up almost every answer {Trước khi vào bài, hãy hiểu năm công cụ tạo nên gần như mọi đáp án}. Read this section slowly — everything later is just these five, combined {Đọc kỹ phần này — mọi thứ sau chỉ là năm cái này ghép lại}.
1. Generics + constraints — a “type function” {Generic + ràng buộc — một “hàm type”}
A generic is a type that takes a type as input, like a function takes a value {Generic là type nhận type làm đầu vào, như hàm nhận giá trị}. extends here means “the input must be at least this” — a constraint, not a condition {extends ở đây nghĩa là “đầu vào phải ít nhất là cái này” — một ràng buộc, không phải điều kiện}.
type Box<T extends string> = { value: T };
// T extends string = "you may only pass a string into Box"
type A = Box<'hi'>; // ✓ { value: 'hi' }
type B = Box<number>; // ❌ error: number is not a string
2. Conditional types — type-level if/else {Conditional type — if/else ở tầng type}
A extends B ? X : Y reads as: “if A is assignable to B, the type is X, otherwise Y” {A extends B ? X : Y đọc là: “nếu A gán được cho B thì là X, ngược lại Y”}. “Assignable” roughly means “fits into” {“Assignable” đại khái là “vừa khít vào”}.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // true
type B = IsString<42>; // false
3. infer — pattern-match and capture {infer — khớp mẫu và bắt giá trị}
Inside a conditional, infer X says: “match this shape and capture the piece here into a new variable X” {Trong conditional, infer X nói: “khớp hình dạng này và bắt phần ở đây vào biến mới X”}. It’s like destructuring, but for types {Giống destructuring, nhưng cho type}.
type ElementType<T> = T extends (infer E)[] ? E : never;
// "if T is an array of some E, give me that E"
type A = ElementType<string[]>; // string
type B = ElementType<number>; // never (not an array)
4. Mapped types — iterate over keys {Mapped type — lặp qua các key}
{ [K in Keys]: ... } loops over each key in a union and builds an object {{ [K in Keys]: ... } lặp từng key trong union và dựng object}. With as you can rename or drop keys while iterating {Với as bạn có thể đổi tên hoặc bỏ key khi lặp}. You can also add/remove readonly and ? modifiers {Bạn cũng thêm/bỏ được modifier readonly và ?}.
type Stringify<T> = { [K in keyof T]: string };
type A = Stringify<{ a: number; b: boolean }>; // { a: string; b: string }
5. Template literals + recursion — string math {Template literal + đệ quy — tính toán trên string}
Template literal types build and pattern-match strings; combined with recursion (a type that refers to itself) they can process strings character by character {Template literal type dựng và khớp mẫu string; kết hợp đệ quy (type tự tham chiếu chính nó) chúng xử lý được string từng ký tự}.
type Greet<S extends string> = `Hello, ${S}!`;
type A = Greet<'world'>; // 'Hello, world!'
Keep these five in mind and the rest is composition {Nhớ năm cái này, phần còn lại chỉ là ghép nối}.
Easy {Dễ}
4 · Pick {4 · Pick}
Implement the built-in Pick<T, K> — construct a type by picking the set of properties K from T {Tự cài Pick<T, K> — tạo type bằng cách chọn tập thuộc tính K từ T}.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>;
// Expected: { title: string; completed: boolean }
Show answer {Hiện đáp án}
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};Step by step {Từng bước}:
keyof Tproduces the union ofT’s keys — here'title' | 'description' | 'completed'{keyof Ttạo union các key củaT}.K extends keyof Tis a constraint: it guarantees callers can only pass keys that actually exist onT. Pass'foo'and you get a compile error {K extends keyof Tlà ràng buộc: bảo đảm chỉ truyền được key thật sự có trênT}.{ [P in K]: ... }is a mapped type: it walks through each memberPof the unionK(soPis'title', then'completed') {{ [P in K]: ... }là mapped type: nó duyệt từng phần tửPcủa unionK}.T[P]is indexed access — “the type of propertyPonT”. For'title'that’sstring{T[P]là indexed access — “type của thuộc tínhPtrênT”}.
So we rebuild an object containing only the chosen keys, each keeping its original value type {Kết quả là object chỉ chứa các key đã chọn, mỗi key giữ type gốc}.
7 · Readonly {7 · Readonly}
Make every property of T readonly {Biến mọi thuộc tính của T thành readonly}.
type ReadonlyTodo = MyReadonly<Todo>;
// Expected: all props become `readonly`
Show answer {Hiện đáp án}
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};Step by step {Từng bước}:
- We map over all keys this time:
[P in keyof T]{Lần này map qua mọi key:[P in keyof T]}. - We copy each value type unchanged with
T[P]{Copy nguyên type mỗi value bằngT[P]}. - The new part: the
readonlymodifier in front of the key applies it to every property {Phần mới: modifierreadonlytrước key áp cho mọi thuộc tính}.
Good to know {Nên biết}: modifiers can be removed too. -readonly strips readonly, and -? makes optional properties required — useful in challenges like a Mutable<T> {Modifier cũng bỏ được. -readonly gỡ readonly, -? biến optional thành bắt buộc}.
11 · Tuple to Object {11 · Tuple to Object}
Turn a tuple of literals into an object whose keys and values are those literals {Biến một tuple literal thành object có key và value là chính các literal đó}.
const tuple = ['tesla', 'model 3', 'model x'] as const;
type Result = TupleToObject<typeof tuple>;
// Expected: { tesla: 'tesla'; 'model 3': 'model 3'; 'model x': 'model x' }
Show answer {Hiện đáp án}
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[P in T[number]]: P;
};The key concept — T[number] {Khái niệm cốt lõi — T[number]}:
A tuple like ['a', 'b'] can be indexed by a specific position (T[0] is 'a') {Một tuple như ['a', 'b'] có thể lập chỉ mục theo vị trí (T[0] là 'a')}. But if you index by the type number instead of a single number, TypeScript returns the type of any position — i.e. the union of all elements {Nhưng nếu lập chỉ mục bằng type number thay vì một số cụ thể, TypeScript trả về type của bất kỳ vị trí nào — tức union của mọi phần tử}. So T[number] here is 'tesla' | 'model 3' | 'model x' {Vậy T[number] ở đây là 'tesla' | 'model 3' | 'model x'}.
We then map over that union, using each member P as both the key and the value {Rồi ta map qua union đó, dùng mỗi phần tử P làm cả key lẫn value}. The constraint (string | number | symbol)[] (the PropertyKey types) is required because only those types are valid object keys {Ràng buộc (string | number | symbol)[] là bắt buộc vì chỉ các type đó mới là key object hợp lệ}.
14 · First of Array {14 · First of Array}
Get the first element’s type, or never for an empty array {Lấy type phần tử đầu, hoặc never nếu mảng rỗng}.
type A = First<[3, 2, 1]>; // 3
type B = First<[]>; // never
Show answer {Hiện đáp án}
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;Step by step {Từng bước}:
[infer F, ...any[]]is a tuple pattern. It reads: “a tuple whose first slot is some type — capture it asF— followed by any number of other elements” {[infer F, ...any[]]là mẫu tuple. Đọc là: “tuple có ô đầu là một type nào đó — bắt làmF— rồi theo sau là bao nhiêu phần tử khác cũng được”}.- For
[3, 2, 1]the pattern matches andFis captured as3{Với[3, 2, 1], mẫu khớp vàFbắt được là3}. - For
[]there is no first element, so the pattern fails and we fall tonever{Với[]không có phần tử đầu nên mẫu thất bại, rơi xuốngnever}.
Why never and not undefined? {Tại sao là never chứ không phải undefined?} Because at the type level “no such element exists” is best represented by never — the type with no values {Vì ở tầng type, “không tồn tại phần tử nào” được biểu diễn tốt nhất bằng never — type không có giá trị nào}.
18 · Length of Tuple {18 · Length of Tuple}
Get a tuple’s length as a number literal {Lấy độ dài tuple dưới dạng number literal}.
type N = Length<['a', 'b', 'c']>; // 3
Show answer {Hiện đáp án}
type Length<T extends readonly any[]> = T['length'];The concept — tuples know their own length {Khái niệm — tuple biết độ dài của chính nó}:
A tuple type stores its length as a literal number in a hidden length property. ['a','b','c']['length'] is exactly 3, not the general number {Type tuple lưu độ dài dưới dạng number literal trong thuộc tính ẩn length. ['a','b','c']['length'] đúng là 3, không phải number chung chung}.
Indexed access T['length'] reads that literal {Indexed access T['length'] đọc literal đó}. Important caveat: a regular array like string[] has length: number (it could be any size), so this trick only gives a precise number for tuples, not open-ended arrays {Lưu ý: mảng thường như string[] có length: number (kích thước bất kỳ), nên mẹo này chỉ cho số chính xác với tuple, không phải mảng mở}.
533 · Concat {533 · Concat}
Concatenate two tuples {Nối hai tuple}.
type R = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
Show answer {Hiện đáp án}
type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U];The concept — variadic (spread) tuple types {Khái niệm — variadic tuple (spread tuple)}:
Just like you spread arrays in values with [...a, ...b], you can spread tuple types the same way at the type level {Giống spread mảng ở tầng giá trị với [...a, ...b], bạn spread được type tuple y như vậy ở tầng type}. [...T, ...U] lays out every element of T then every element of U into one new tuple, preserving order and literal types {[...T, ...U] trải mọi phần tử của T rồi tới U thành một tuple mới, giữ nguyên thứ tự và literal type}.
This is the foundation for most tuple manipulation challenges (Push, Unshift, Reverse, …) {Đây là nền cho hầu hết bài thao tác tuple (Push, Unshift, Reverse, …)}.
43 · Exclude {43 · Exclude}
Exclude from union T those types assignable to U {Loại khỏi union T những type gán được cho U}.
type R = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
Show answer {Hiện đáp án}
type MyExclude<T, U> = T extends U ? never : T;The big concept — distributive conditional types {Khái niệm lớn — distributive conditional type}:
This one-liner only works because of a special rule {Dòng này chạy được nhờ một quy tắc đặc biệt}: when the type being checked (the left side of extends) is a bare type parameter (just T, not wrapped) and it’s a union, the conditional is applied to each member separately, then the results are unioned back {khi type được kiểm tra (vế trái extends) là tham số trần (chỉ T, không bọc) và là union, conditional được áp cho từng phần tử riêng, rồi gộp kết quả lại}.
Watch it unfold for 'a' | 'b' | 'c' excluding 'a' {Xem nó bung ra với 'a' | 'b' | 'c' loại 'a'}:
// distributes into three independent checks:
('a' extends 'a' ? never : 'a') // → never
| ('b' extends 'a' ? never : 'b') // → 'b'
| ('c' extends 'a' ? never : 'c') // → 'c'
// = never | 'b' | 'c'
// = 'b' | 'c' (never vanishes from a union)Two facts to remember {Hai điều cần nhớ}: distribution needs a bare parameter (wrapping in [T] turns it off), and never always disappears from a union {distribution cần tham số trần (bọc [T] sẽ tắt), và never luôn biến mất khỏi union}.
189 · Awaited {189 · Awaited}
Recursively unwrap the value type of a Promise-like type {Bóc đệ quy type giá trị của một type giống Promise}.
type R = MyAwaited<Promise<Promise<string>>>; // string
Show answer {Hiện đáp án}
type MyAwaited<T extends PromiseLike<any>> =
T extends PromiseLike<infer V>
? V extends PromiseLike<any>
? MyAwaited<V>
: V
: never;Step by step {Từng bước}:
T extends PromiseLike<infer V>matches a promise and captures its resolved type asV{T extends PromiseLike<infer V>khớp một promise và bắt type đã resolve vàoV}.- Then we ask: is
Vitself another promise? {Rồi hỏi: bản thânVcó lại là promise không?}- If yes → recurse: call
MyAwaited<V>to peel the next layer {Nếu có → đệ quy: gọiMyAwaited<V>để bóc lớp tiếp}. - If no → we’ve reached the real value, return
V(this is the base case that stops recursion) {Nếu không → đã tới giá trị thật, trả vềV(đây là base case dừng đệ quy)}.
- If yes → recurse: call
For Promise<Promise<string>>: first V = Promise<string> (still a promise → recurse), then V = string (not a promise → return string) {Với Promise<Promise<string>>: lần đầu V = Promise<string> (vẫn là promise → đệ quy), lần sau V = string (không phải promise → trả string)}. Every recursion must have a base case, or the type would expand forever {Mọi đệ quy phải có base case, nếu không type sẽ bung vô tận}.
Medium {Trung bình}
2 · Get Return Type {2 · Get Return Type}
Implement ReturnType<T> without using the built-in {Cài ReturnType<T> mà không dùng bản built-in}.
type R = MyReturnType<() => 'hello'>; // 'hello'
Show answer {Hiện đáp án}
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;The concept — infer works anywhere in the pattern {Khái niệm — infer hoạt động ở bất kỳ vị trí nào trong mẫu}:
We match T against a generic function shape: (...args: any[]) => infer R {Ta khớp T với hình dạng hàm tổng quát: (...args: any[]) => infer R}. (...args: any[]) means “accepts any arguments — we don’t care about them” {(...args: any[]) nghĩa là “nhận tham số bất kỳ — ta không quan tâm”}. The interesting part is infer R placed in the return-type position, which captures whatever the function returns {Phần thú vị là infer R đặt ở vị trí return, bắt giá trị hàm trả về}.
The lesson: you can drop infer into any slot of the pattern — parameters, return type, array elements, promise contents — to extract exactly the piece you want {Bài học: bạn có thể đặt infer vào bất kỳ ô nào của mẫu — tham số, return, phần tử mảng, nội dung promise — để rút đúng phần bạn cần}.
3 · Omit {3 · Omit}
Implement Omit<T, K> — T without the keys in K {Cài Omit<T, K> — T bỏ đi các key trong K}.
type R = MyOmit<Todo, 'description'>; // { title: string; completed: boolean }
Show answer {Hiện đáp án}
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};The concept — key remapping with as {Khái niệm — đổi key bằng as}:
A mapped type can rename keys using as: [P in keyof T as NewKey] {Mapped type đổi tên key được bằng as: [P in keyof T as NewKey]}. The crucial trick: if you remap a key to never, that key is dropped from the result {Mẹo then chốt: nếu đổi một key thành never, key đó bị loại khỏi kết quả}.
So we go through every key P of T and compute a new key {Vậy ta duyệt mọi key P của T và tính key mới}:
P extends K ? never : P
// if P is one of the keys to omit → never (drops it)
// otherwise → keep P unchangedFor Omit<Todo, 'description'>: 'title' and 'completed' stay, 'description' maps to never and disappears {'title' và 'completed' giữ lại, 'description' map thành never và biến mất}.
9 · Deep Readonly {9 · Deep Readonly}
Make an object readonly recursively (nested objects too), but leave functions/primitives alone {Biến object readonly đệ quy (cả object lồng nhau), nhưng để nguyên hàm/primitive}.
type X = { a: () => 22; b: string; c: { d: boolean } };
type R = DeepReadonly<X>;
// Expected: a unchanged, b: string, c: { readonly d: boolean }, all readonly
Show answer {Hiện đáp án}
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Record<string, unknown>
? DeepReadonly<T[K]>
: T[K];
};Step by step {Từng bước}:
- Start like plain
Readonly: map over every key and addreadonly{Bắt đầu nhưReadonlythường: map mọi key và thêmreadonly}. - For each value, decide whether to dive deeper {Với mỗi value, quyết định có đào sâu hơn không}. The test
T[K] extends Record<string, unknown>asks “is this value a plain object?” {Phép thửT[K] extends Record<string, unknown>hỏi “value này có phải object thuần không?”}.- Object → recurse with
DeepReadonly<T[K]>so nested objects also become readonly {Object → đệ quy vớiDeepReadonly<T[K]>để object lồng cũng readonly}. - Function or primitive (
string,boolean, a function type, …) → it doesn’t matchRecord<string, unknown>, so we return it unchanged {Hàm hoặc primitive → không khớpRecord<string, unknown>, nên trả về nguyên}.
- Object → recurse with
This is recursion over a data structure (the object tree) rather than over a list — the base case is “the value isn’t an object” {Đây là đệ quy trên cấu trúc dữ liệu (cây object) chứ không phải danh sách — base case là “value không phải object”}.
10 · Tuple to Union {10 · Tuple to Union}
Convert a tuple type into the union of its element types {Chuyển tuple thành union các phần tử}.
type R = TupleToUnion<[123, '456', true]>; // 123 | '456' | true
Show answer {Hiện đáp án}
type TupleToUnion<T extends readonly any[]> = T[number];The concept — reuse T[number] {Khái niệm — tái dùng T[number]}:
Same idea you saw in Tuple to Object {Cùng ý tưởng trong Tuple to Object}. Indexing a tuple type with the number type asks “what is the type at any index?”, and the answer is the union of every element {Lập chỉ mục type tuple bằng type number hỏi “type ở bất kỳ chỉ mục nào?”, và đáp án là union mọi phần tử}. So [123, '456', true][number] is 123 | '456' | true {Vậy [123, '456', true][number] là 123 | '456' | true}.
Recognizing this small reusable pattern is half of getting good at type challenges {Nhận ra những mẫu nhỏ tái dùng được như vậy là một nửa của việc giỏi type challenges}.
110 · Capitalize {110 · Capitalize}
Capitalize the first letter of a string type {Viết hoa chữ cái đầu của một string type}.
type R = MyCapitalize<'hello world'>; // 'Hello world'
Show answer {Hiện đáp án}
type MyCapitalize<S extends string> = S extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}`
: S;The concept — splitting strings with template literals {Khái niệm — tách string bằng template literal}:
`${infer First}${infer Rest}` is a string pattern {`${infer First}${infer Rest}` là một mẫu string}. TypeScript matches it greedily so that First grabs one character and Rest grabs everything after {TypeScript khớp sao cho First lấy một ký tự và Rest lấy phần còn lại}. For 'hello world': First = 'h', Rest = 'ello world' {Với 'hello world': First = 'h', Rest = 'ello world'}.
Then we use the built-in intrinsic Uppercase<> on just the first character and glue the string back together: `${Uppercase<'h'>}${'ello world'}` → 'Hello world' {Rồi dùng intrinsic có sẵn Uppercase<> lên đúng ký tự đầu và dán string lại}. The empty string '' doesn’t match the two-part pattern, so it falls through to S unchanged — that’s the base case {Chuỗi rỗng '' không khớp mẫu hai phần nên rơi xuống S nguyên — đó là base case}.
296 · Permutation {296 · Permutation}
Produce every permutation of a union as tuples {Sinh mọi hoán vị của một union dưới dạng tuple}.
type R = Permutation<'A' | 'B' | 'C'>;
// ['A','B','C'] | ['A','C','B'] | ['B','A','C'] | ... (6 tuples)
Show answer {Hiện đáp án}
type Permutation<T, K = T> = [T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never;This combines two advanced tricks. Take them one at a time {Bài này gộp hai mẹo nâng cao. Đi từng cái}:
Trick 1 — keeping a copy of the union with K = T {Mẹo 1 — giữ bản sao của union với K = T}. We default a second parameter K to T. We need this because the next line, K extends K, deliberately triggers distribution over K, and we want a stable reference to the whole original union (T) to subtract from {Ta đặt mặc định tham số thứ hai K = T. Cần vậy vì dòng K extends K cố tình kích hoạt distribution trên K, và ta muốn một tham chiếu ổn định tới toàn bộ union gốc (T) để trừ đi}.
Trick 2 — [T] extends [never] to detect the empty union {Mẹo 2 — [T] extends [never] để phát hiện union rỗng}. We need a base case: when there’s nothing left to permute, return [] {Cần base case: khi không còn gì để hoán vị, trả []}. But you cannot write T extends never ? ... directly, because if T is a union (or never) it distributes and misbehaves {Nhưng không thể viết T extends never ? ... trực tiếp, vì nếu T là union (hoặc never) nó sẽ distribute và sai}. Wrapping both sides in a tuple, [T] extends [never], turns distribution off so we get a plain, correct check {Bọc cả hai vế trong tuple, [T] extends [never], tắt distribution để có phép kiểm tra đúng}.
Putting it together {Ghép lại}: K extends K distributes over each member K. For each one we place K at the front and recurse on the remaining members Exclude<T, K> {K extends K distribute trên từng phần tử K. Với mỗi cái, ta đặt K lên đầu và đệ quy trên phần còn lại Exclude<T, K>}:
// for 'A' | 'B' | 'C':
['A', ...Permutation<'B' | 'C'>] // → ['A','B','C'] | ['A','C','B']
| ['B', ...Permutation<'A' | 'C'>] // → ['B','A','C'] | ['B','C','A']
| ['C', ...Permutation<'A' | 'B'>] // → ['C','A','B'] | ['C','B','A']Hard {Khó}
55 · Union to Intersection {55 · Union to Intersection}
Convert a union A | B into an intersection A & B {Chuyển union A | B thành intersection A & B}.
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }
Show answer {Hiện đáp án}
type UnionToIntersection<U> = (
U extends any ? (arg: U) => void : never
) extends (arg: infer I) => void
? I
: never;This is the famous one. The trick is contravariance {Bài kinh điển. Mẹo nằm ở contravariance (nghịch biến)}.
Background concept — contravariance of function parameters {Khái niệm nền — tính nghịch biến của tham số hàm}. Function parameter positions are contravariant: a function that accepts A is compatible only with code expecting A or something more specific. When TypeScript must find one parameter type that satisfies several function types at once, it combines them with & (intersection), not | {Vị trí tham số hàm là nghịch biến: hàm nhận A chỉ tương thích với nơi mong đợi A hoặc cụ thể hơn. Khi TypeScript phải tìm một type tham số thoả nhiều type hàm cùng lúc, nó gộp bằng & (intersection), không phải |}.
Step by step {Từng bước}:
U extends any ? (arg: U) => void : neverdistributesUinto a union of functions:((arg: {a:1}) => void) | ((arg: {b:2}) => void){distributeUthành union các hàm}.- We then try to
infer Ias the single parameter type from that whole union of functions:... extends (arg: infer I) => void{Rồi tainfer Ilàm type tham số duy nhất từ cả union hàm đó}. - Because of contravariance, the only
Ithat works for both functions is the intersection{a:1} & {b:2}{Nhờ nghịch biến,Iduy nhất hợp cho cả hai hàm là intersection{a:1} & {b:2}}.
You don’t need to derive this from scratch each time — recognize the (U extends any ? (arg: U) => void : never) shape as the canonical “union → intersection” gadget {Không cần tự suy lại mỗi lần — nhận ra hình dạng (U extends any ? (arg: U) => void : never) là “đồ nghề” chuẩn cho “union → intersection”}.
114 · CamelCase {114 · CamelCase}
Convert a kebab-case string type to camelCase {Chuyển string kebab-case thành camelCase}.
type R = CamelCase<'foo-bar-baz'>; // 'fooBarBaz'
type E = CamelCase<'foo--bar'>; // 'foo-Bar' (edge case preserved)
Show answer {Hiện đáp án}
type CamelCase<S extends string> = S extends `${infer Head}-${infer Tail}`
? Tail extends Capitalize<Tail>
? `${Head}-${CamelCase<Tail>}`
: `${Head}${CamelCase<Capitalize<Tail>>}`
: S;Step by step {Từng bước}:
`${infer Head}-${infer Tail}`splits the string at the first dash {tách string ở dấu gạch đầu tiên}. For'foo-bar-baz':Head = 'foo',Tail = 'bar-baz'{Với'foo-bar-baz':Head = 'foo',Tail = 'bar-baz'}.- The inner check
Tail extends Capitalize<Tail>handles the tricky double-dash case {Phép kiểm tra trongTail extends Capitalize<Tail>xử lý ca hai dấu gạch}. IfTailis already capitalized, it means the character after the dash was another dash (e.g.'foo--bar'→Tail = '-bar', andCapitalize<'-bar'>is still'-bar') {NếuTailđã viết hoa, tức ký tự sau dấu gạch là một dấu gạch nữa}. In that case we keep the literal dash and recurse:`${Head}-${CamelCase<Tail>}`{Khi đó giữ dấu gạch và đệ quy}. - Otherwise (the normal case) we capitalize the first letter of
Tailand recurse without the dash:`${Head}${CamelCase<Capitalize<Tail>>}`{Ngược lại (ca thường) viết hoa chữ đầu củaTailvà đệ quy bỏ dấu gạch}. - When no dash remains, the pattern fails and we return
S— the base case {Khi hết dấu gạch, mẫu thất bại và trả vềS— base case}.
Trace 'foo-bar-baz': foo + Camel(Bar-baz) → foo + Bar + Camel(Baz) → fooBarBaz {Lần theo 'foo-bar-baz': foo + Camel(Bar-baz) → foo + Bar + Camel(Baz) → fooBarBaz}.
How to Practice {Cách luyện tập}
- Read the test cases first {Đọc test case trước}: they define the exact behavior — including edge cases like
foo--bar{chúng định nghĩa hành vi chính xác — kể cả edge case nhưfoo--bar}. - Reach for the five building blocks {Dùng năm khối nền tảng}: most solutions are a conditional +
infer, a mapped type, or recursion {hầu hết đáp án là conditional +infer, mapped type, hoặc đệ quy}. - Watch for distribution {Để ý distribution}: bare type params over unions distribute; wrap in
[T]to stop it {tham số trần trên union sẽ phân phối; bọc[T]để dừng}. - Always find the base case {Luôn tìm base case}: every recursive type needs a stopping condition (empty tuple, empty string, “not an object”) {mọi type đệ quy cần điều kiện dừng (tuple rỗng, string rỗng, “không phải object”)}.
- Use the Playground {Dùng Playground}: hover a type and watch it resolve step by step {rê chuột lên type và xem nó resolve từng bước}.
The skill compounds fast {Kỹ năng này tích luỹ nhanh}: once infer + conditional + recursion click, you can read (and write) the gnarliest utility types with confidence {một khi infer + conditional + đệ quy đã thông, bạn đọc (và viết) được những utility type hiểm hóc nhất một cách tự tin}.
Source & full set {Nguồn & bộ đầy đủ}: type-challenges on GitHub (MIT). Related: Unicode, Strings & Binary in JavaScript.