Menu

Kiểu hàm trong TypeScript: cú pháp mũi tên và callback

Cách viết kiểu của một hàm trong TypeScript: cú pháp mũi tên (a: number) => string, type alias cho hàm, khai báo kiểu cho arrow function và callback, call signature và construct signature, và vì sao kiểu Function quá lỏng lẻo.

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.

Kiểu hàm mô tả các tham số và giá trị trả về của một hàm, viết bằng mũi tên: (a: number, b: number) => number. Đặt tên cho nó bằng type là bạn dùng được cho biến, tham số và thuộc tính của đối tượng.

Hàm được gán cho add không cần chú thích kiểu: kiểu của các tham số lấy từ Operation. Cơ chế này gọi là contextual typing, và đó là lý do chính để đặt tên cho kiểu hàm.

Cú pháp kiểu hàm

Dạng chung là (parameters) => ReturnType. Mỗi tham số cần một tên và một kiểu. Tên chỉ để dễ đọc và để trình soạn thảo gợi ý; mọi hàm có kiểu tham số tương thích đều khớp, dù nó đặt tên tham số là gì.

type Predicate = (value: number) => boolean;
type Formatter = (value: number, digits?: number) => string; // optional parameter
type Logger = (...parts: string[]) => void;                    // rest parameter
type Factory = () => { id: number };                           // no parameters

Tên không phải là tùy chọn. (string) => void khai báo một tham số tên là string không có kiểu, và chế độ strict từ chối nó với TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?. Hãy viết (value: string) => void.

Khai báo kiểu cho arrow function

Có hai cách khai báo kiểu cho một arrow function. Chú thích kiểu ngay trên hàm, hoặc chú thích biến bằng một kiểu hàm và để tham số được suy ra.

Dạng 1 là lựa chọn thường dùng cho một hàm đứng riêng. Dạng 3 có lợi khi nhiều hàm dùng chung một chữ ký (handler, hàm so sánh, hàm chuyển đổi), vì chữ ký chỉ viết một lần và mọi thay đổi trên nó đều được kiểm tra ở khắp nơi.

Kiểu callback

Một tham số có kiểu là kiểu hàm chính là một callback. Hàm của nơi gọi được kiểm tra theo kiểu đó, và tham số của hàm được suy ra từ nó.

Hai quy tắc giúp callback dễ dùng:

  • Ít tham số hơn cũng được. Một hàm nhận một tham số có thể truyền vào chỗ cung cấp hai tham số. JavaScript bỏ qua đối số thừa, nên TypeScript cho phép điều này. Nhiều tham số hơn số mà kiểu cung cấp thì là lỗi (Target signature provides too few arguments.).
  • Kiểu trả về void nhận mọi thứ. Một callback có kiểu (...) => void vẫn có thể trả về giá trị; nơi gọi cam kết không dùng đến nó. Đó là lý do list.forEach((x) => other.push(x)) biên dịch được dù push trả về một số.

Kiểu của tham số vẫn phải tương thích. Truyền (x: string) => ... vào chỗ cần (a: number, b: number) => number sẽ thất bại với Types of parameters 'x' and 'a' are incompatible.

Call signature

(n: number) => string là cách viết tắt của một kiểu đối tượng có call signature: { (n: number): string }. Bạn cần dạng đầy đủ khi hàm còn mang theo thuộc tính.

TypeScript cho phép thêm thuộc tính vào một hàm khai báo bằng const trong cùng phạm vi, và theo dõi chúng trong kiểu của hàm. Một interface cũng chứa được call signature: interface Counter { (): number; count: number }.

Construct signature mô tả thứ được gọi bằng new. Nó thêm new vào phía trước: new (name: string) => User, hoặc { new (name: string): User } ở dạng đối tượng. Đây là cách khai báo kiểu cho một tham số nhận vào một lớp.

Cú pháp phương thức và cú pháp thuộc tính

Trong một kiểu đối tượng, thành viên là hàm có thể viết dưới dạng phương thức, handle(value: string): void, hoặc dưới dạng thuộc tính chứa hàm, handle: (value: string) => void. Trông thì có vẻ thay thế được cho nhau nhưng khi bật strict chúng được kiểm tra khác nhau:

Trình biên dịch báo:

index.ts(8,27): error TS2322: Type '(value: string) => void' is not assignable to type '(value: string | number) => void'.
  Types of parameters 'value' and 'value' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

Lỗi này đúng: onlyStrings không xử lý được số. Dạng phương thức để lọt cùng sai sót đó (tham số của phương thức được kiểm tra theo kiểu "bivariant" vì lý do lịch sử), và a.handle(42) sẽ gây lỗi lúc chạy với TypeError: value.toUpperCase is not a function. Xóa dòng b để thấy lỗi đó. Khi tự viết kiểu đối tượng, dạng thuộc tính bắt được nhiều lỗi hơn.

Tránh dùng kiểu Function

Function là kiểu có sẵn mà mọi giá trị hàm đều thỏa mãn. Nó gần như không bao giờ là thứ bạn cần: TypeScript không biết tham số hay kiểu trả về, nên mọi lời gọi đều được chấp nhận và trả về any.

Hãy thay Function bằng chữ ký thật. Khi bạn thật sự nhận mọi hàm (một debounce tổng quát, một hàm bọc để ghi log), (...args: never[]) => unknown nhận mọi hàm mà vẫn giữ kết quả là unknown thay vì any. Với những hàm có kiểu phụ thuộc vào đầu vào, xem generic: kiểu của một hàm generic trông như <T>(value: T) => T.

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

Làm sao để định nghĩa kiểu hàm trong TypeScript?

Dùng cú pháp mũi tên: (a: number, b: number) => number. Đặt tên cho nó bằng type alias, type Compare = (a: number, b: number) => number;, rồi dùng tên đó cho biến, tham số và thuộc tính. Tên tham số là một phần của cú pháp nhưng chỉ để làm tài liệu: một hàm có tên tham số khác vẫn khớp.

Làm sao để khai báo kiểu cho tham số callback trong TypeScript?

Viết kiểu hàm làm kiểu của tham số: function onEach(items: string[], cb: (item: string, index: number) => void) { ... }. Nơi gọi có thể truyền một hàm nhận ít tham số hơn, và tham số của callback được suy ra từ kiểu đó, nên onEach(list, (item) => ...) không cần chú thích kiểu.

Vì sao không nên dùng kiểu Function trong TypeScript?

Function nhận mọi hàm và lời gọi đến nó không được kiểm tra: đối số nào cũng được và kết quả là any. Hãy viết chữ ký thật, như () => void hoặc (value: string) => number, hoặc (...args: never[]) => unknown khi bạn thật sự nhận mọi hàm.

Kiểu hàm và call signature khác nhau thế nào?

Chúng mô tả cùng một thứ. (n: number) => string là cách viết tắt của kiểu đối tượng { (n: number): string }. Dạng call signature cần thiết khi hàm còn có thuộc tính, ví dụ { (n: number): string; label: string }.

Làm sao để khai báo kiểu cho arrow function trong TypeScript?

Hoặc chú thích kiểu cho tham số và giá trị trả về ngay trong hàm, const half = (n: number): number => n / 2;, hoặc gán cho biến một kiểu hàm và để tham số được suy ra: const half: (n: number) => number = (n) => n / 2;.

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