TypeScript widen literal về string khi T không có constraint và không đứng riêng một mình ở kiểu trả về — ví dụ hàm trả { v } hay T[].
- Trả về đúng
Tthì literal được giữ. - Muốn giữ literal trong mọi trường hợp thì cho constraint hoặc dùng
consttype parameter.
ts
function box<T>(v: T) { return { v } }
box('a') // T = string — T không đứng trần ở kiểu trả về nên bị widen
function tag<T>(v: T) { return v }
tag('a') // T = 'a' — trả về T trần thì giữ literal
function box2<T extends string>(v: T) { return { v } }
box2('a') // T = 'a' (constraint kéo suy luận về literal)
function list<const T extends readonly unknown[]>(v: T) { return v }
list(['a', 'b']) // T = readonly ['a', 'b'] (TS 5.0+)Ba công cụ hay dùng:
- T extends string (hoặc extends number, extends PropertyKey) — thủ thuật cổ điển để suy luận literal.
- const type parameter (TS 5.0) — hoạt động như thể nơi gọi có as const, tiện cho hàm nhận mảng/object config.
- NoInfer<T> (TS 5.4) — chặn một vị trí tham gia suy luận, để T chỉ được suy từ vị trí bạn muốn:
ts
function createState<T extends string>(initial: T, values: NoInfer<T>[]) {}
createState('a', ['a', 'b']) // Lỗi ở 'b' — T chỉ suy từ initial nên T = 'a'; bỏ NoInfer thì T = 'a' | 'b', không lỗiLưu ý — chiều ngược lại: viết T extends string rồi trả về T sẽ khiến consumer nhận literal quá hẹp cho những chỗ họ muốn string.
Chỉ ép hẹp khi nơi gọi thực sự cần literal (key lookup, template literal type, exhaustiveness).