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 infer ở bấ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;
// ① ② ③ ④ ⑤ ⑥ ⑦
- một alias generic nhận type
Tbất kỳ. - bắt đầu conditional: “
Tcó khớp mẫu này không?”. - mẫu:
Promisemà đối số type ta bắt thànhV. - giới thiệu biến type mới; compiler điền bằng thứ đã nằm ở ô đó.
- nếu khớp thành công, kết quả là
V(type bên trong). - nếu khớp thất bại, trả
Tnguyê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
inferchỉ gán trong mệnh đềextends— không viếtT 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
infertrong 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ánhfalsechạ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;- ràng buộc: chỉ callable type mới vào utility này.
- conditional thứ hai: khớp hình dạng
T; bắt cả tuple tham số thànhP. - bỏ qua return;
anykhớp mọi thứ. - thành công: đáp án là tuple tham số đã bắt.
- 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 MyReturnType ở Phầ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 Pbắt cả danh sách thành một tuple, kể cả label và optional. - Hàm overload —
Parameters(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;Tphải là array/tuple.- 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. - ô “mọi thứ trước cuối”; ta bỏ.
- ô cuối; compiler gán type phần tử cuối.
- 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 : [];- bắt tất cả trừ cuối vào
Rest; phần cuối phải tồn tại và khớpany. - tuple prefix ta muốn trả.
- không
infer— ta vứt type cụ thể của phần tử cuối. - tuple rỗng không khớp (không có ô cuối) → trả
[], không phảinever, 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
Shift—Shiftdùng[any, ...infer Rest]bỏ phần tử đầu; cùng mẹo, đầu đối diện. nevervs[]khi thất bại —Lasttrảnever(không có giá trị);Poptrả[](tuple rỗng vẫn là kết quả tuple hợp lệ).- Bốn thao tác bóc tuple —
First,Last,Pop,Shiftcù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;Fnphải gọi được.- bắt tham số hiện có thành tuple
P. - bắt return thành
R. - hàm mới: spread tham số cũ, rồi
A, cùng return. - 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 cosmeticCạm bẫy
- Nhiều
infertrong một mẫu — mọi gán xảy ra trong một lần khớp; thứ tựinfer Pvsinfer Rkhô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 string — ToNumber<'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:
- Khớp cả string như mẫu template literal rồi bắt substring.
- Ràng buộc phần bắt:
N extends numberphả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;- input là string literal type.
- mẫu: cả string phải infer được thành literal
number. - bắt
N, nhưng chỉ khi text bắt được là literal số hợp lệ và thu hẹp thànhnumber. - thành công:
Nlà42, không phải'42'. - 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 literal —
infer Nthuần giữ'42';infer N extends numbercó ràng buộc cho42. - 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;- union ba ký tự ta coi là “rác đầu”.
- đoạn đầu phải là một trong các ký tự đó.
- bắt mọi thứ sau ký tự đầu.
- đệ quy: thử cắt tiếp từ string ngắn hơn.
- 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).
TrimRightlà đối xứng — đặtinfer Resttrước whitespace:S extends `${infer Rest}${Whitespace}`.- Ghép
TrimRightthànhTrim— ta đào sâu string type ở Phần 7.
Whitespace là union 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 extends là vế trái của destructuring type — infer 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 Xsống trong mệnh đềextendsvà 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ềuinfertrong một mẫu là bình thường. infercó 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.