Menu

Suy luận kiểu trong TypeScript: khi nào cần chú thích kiểu

TypeScript tự suy ra phần lớn kiểu từ các giá trị bạn viết. Tìm hiểu kiểu được suy ra cho biến, let và const, đối tượng, mảng và giá trị trả về, cách callback nhận kiểu từ ngữ cảnh, và những chỗ bạn vẫn cần chú thích kiểu.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Suy luận kiểu (type inference) là việc TypeScript tự tìm ra một kiểu mà bạn không viết. Khởi tạo một biến thì nó nhận kiểu của giá trị; trả về một giá trị từ hàm thì kiểu trả về đi theo. Phần lớn code TypeScript có ít chú thích kiểu hơn nhiều so với người ta nghĩ.

Các chú thích trong code cho thấy trình soạn thảo hiển thị gì khi bạn rê chuột lên từng tên. Dòng cuối là lỗi biên dịch (TS2322) dù count chưa từng được chú thích kiểu; // @ts-expect-error đánh dấu lỗi đó là có chủ ý để phần còn lại của khối lệnh vẫn chạy.

let và const: mở rộng literal

Một const không bao giờ thay đổi, nên TypeScript cho nó kiểu hẹp nhất: chính giá trị literal đó. Một let nhận kiểu rộng hơn, vì nó có thể được gán lại sau này.

let a = "left";    // string
const b = "left";  // "left"
let c = 42;        // number
const d = 42;      // 42
const e = true;    // true

Điều này quan trọng khi một hàm chỉ chấp nhận một số giá trị nhất định:

index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.

Sửa bằng const, hoặc chú thích kiểu cho biến: let dir: "left" | "right" = "left". Literal type có trang riêng, literal type.

Đối tượng và mảng mở rộng kiểu của thành viên

Thuộc tính của object literal có thể thay đổi, nên chúng bị mở rộng giống let, kể cả khi bản thân đối tượng là const. Phần tử mảng cũng vậy. as const biến mọi thứ thành readonly và giữ nguyên các literal type.

Suy luận kiểu trả về

Kiểu trả về của một hàm được suy ra từ các câu lệnh return. Khi có nhiều câu lệnh, kết quả là union của chúng.

Kiểu trả về được suy ra là đủ dùng cho hầu hết các hàm. Hãy viết tường minh kiểu trả về cho những hàm được export hoặc công khai khi bạn muốn chữ ký hàm là một cam kết: khi đó, một thay đổi trong thân hàm khiến nó trả về kiểu khác sẽ báo lỗi ngay tại hàm, chứ không phải ở một nơi gọi xa xôi nào đó.

Kiểu theo ngữ cảnh: callback nhận được kiểu

Tham số không được suy ra từ cách hàm được gọi. Có hai ngoại lệ. Một tham số có giá trị mặc định nhận kiểu của giá trị đó (function retry(times = 3) khiến times là number). Và một hàm được viết ở nơi đã có sẵn một kiểu hàm mong đợi sẽ nhận kiểu tham số từ ngữ cảnh đó, nên tham số của callback không cần chú thích kiểu.

Điều tương tự áp dụng cho forEach, filter, reduce, callback của Promise, trình xử lý sự kiện và mọi hàm bạn truyền vào một tham số có kiểu hàm đã khai báo.

Tham số cần chú thích kiểu

Không có giá trị mặc định hay ngữ cảnh như vậy, TypeScript không nhìn vào cách hàm được gọi để đoán kiểu tham số. Khi bật strict, một tham số không có chú thích kiểu là lỗi TS7006:

index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.

Viết function double(x: number) thì khối lệnh in ra 42, với kiểu trả về number được suy ra từ x * 2.

Kiểu chung tốt nhất

Với một mảng literal chứa các loại giá trị khác nhau, TypeScript suy ra một mảng có kiểu là union của các kiểu đó. Nó không đi tìm một kiểu cơ sở chung mà bạn không nhắc tới.

Mảng rỗng là trường hợp cần để ý. const list = [] bắt đầu là một mảng "tiến hóa" có kiểu lớn dần theo mỗi lần push; một chú thích kiểu, const list: string[] = [], nói rõ ý định ngay từ đầu và dễ hiểu hơn.

Khi nào cần chú thích kiểu

Tình huốngChú thích kiểu?
Biến được khởi tạo bằng một giá trịKhông, giá trị cho biết kiểu
Biến khai báo mà không có giá trị (let result;)Có, let result: string;
Tham số của hàmCó (trừ callback có ngữ cảnh, hoặc có giá trị mặc định)
Kiểu trả về của một hàm nội bộ nhỏTùy chọn
Kiểu trả về của hàm được export hoặc công khaiNên có, như một cam kết
Mảng hoặc đối tượng rỗng sẽ được điền sauCó, const ids: number[] = []
Giá trị cần giữ literal typeDùng as const
Giá trị phải khớp một kiểu nhưng giữ kiểu được suy ra của nóDùng satisfies

Cách nhanh nhất để xem kiểu được suy ra là rê chuột lên tên đó trong trình soạn thảo. Nếu không có trình soạn thảo, hãy gán giá trị cho một biến có kiểu sai, như const probe: boolean = mixed;, và thông báo lỗi của trình biên dịch sẽ nêu tên kiểu được suy ra.

Câu hỏi thường gặp

Suy luận kiểu trong TypeScript là gì?

Là việc trình biên dịch tự tìm ra một kiểu mà bạn không viết. let count = 0 cho count kiểu number, và một hàm trả về a + b với hai số sẽ có kiểu trả về number. Kiểu được suy ra được kiểm tra y như kiểu viết tay.

Có nên chú thích kiểu cho mọi biến trong TypeScript không?

Không. Hãy chú thích tham số của hàm (chúng không được suy ra từ lời gọi, chỉ từ giá trị mặc định hoặc ngữ cảnh callback), kiểu trả về của hàm được export nếu bạn muốn API ổn định, và các biến khai báo mà không có giá trị. Phần còn lại cứ để TypeScript suy ra: const name: string = "Ada" chỉ lặp lại điều trình biên dịch đã biết.

Vì sao const suy ra literal type còn let suy ra string?

Một const không bao giờ gán lại được, nên const dir = "left" nhận literal type "left". Một let sau này có thể chứa bất kỳ chuỗi nào khác, nên let dir = "left" bị mở rộng thành string. Việc mở rộng tương tự xảy ra với thuộc tính của object literal, vì thuộc tính có thể gán lại; as const ngăn điều đó.

Vì sao TypeScript báo tham số ngầm định có kiểu 'any'?

Đó là lỗi TS7006 từ noImplicitAny, một phần của strict. TypeScript không suy ra kiểu tham số từ cách hàm được gọi, nên một tham số trần sẽ là any. Hãy thêm chú thích kiểu, function double(x: number), hoặc truyền hàm vào nơi đã biết kiểu từ ngữ cảnh, như một callback cho map.

Làm sao xem được kiểu mà TypeScript đã suy ra?

Rê chuột lên tên đó trong trình soạn thảo (VS Code và hầu hết trình soạn thảo hiển thị kiểu được suy ra trong một tooltip). Trong code, gán giá trị cho một biến có kiểu rõ ràng là sai sẽ khiến trình biên dịch in ra kiểu được suy ra trong thông báo lỗi.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu