Mặc định TypeScript suy luận generic ở mức rộng nhất còn hợp lệ, nên T thường thành string chứ không phải 'a'.
Muốn giữ literal thì phải cho constraint hoặc dùng const type parameter.
function tag<T>(v: T) { return v }
tag('a') // T = string
function tag2<T extends string>(v: T) { return v }
tag2('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:
function createState<T>(initial: T, values: NoInfer<T>[]) {}
createState('a', ['a', 'b']) // T suy từ initial, values không kéo T rộng raBẫy 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).