Tìm kiếm trang web

Làm thế nào để kiểm tra loại đối tượng khi chạy trong TypeScript?


Tôi đang cố gắng tìm cách truyền một đối tượng vào hàm và kiểm tra kiểu của nó trong thời gian chạy. Đây là một mã giả:

function func (obj:any) {
    if(typeof obj === "A") {
        // do something
    } else if(typeof obj === "B") {
        //do something else
    }
}

let a:A;
let b:B;
func(a);

Nhưng typeof luôn trả về "object" và tôi không thể tìm cách nào để lấy kiểu thực sự của a hoặc b. instanceof cũng không hoạt động và trả về cùng một kết quả.

Có ý tưởng nào về cách thực hiện điều đó trong TypeScript không?

Câu trả lời #1

Chỉnh sửa: Tôi muốn chỉ ra với những người đến đây từ tìm kiếm rằng câu hỏi này cụ thể liên quan đến các loại không phải lớp, tức là các hình dạng đối tượng được định nghĩa bởi interface hoặc type alias. Đối với các loại lớp, bạn có thể sử dụng instanceof của JavaScript để xác định lớp mà một thể hiện thuộc về, và TypeScript sẽ tự động thu hẹp loại trong trình kiểm tra kiểu.

Các loại được loại bỏ vào thời gian biên dịch và không tồn tại tại thời gian chạy, vì vậy bạn không thể kiểm tra loại tại thời gian chạy.

Điều bạn có thể làm là kiểm tra xem hình dạng của một đối tượng có đúng như bạn mong đợi hay không, và TypeScript có thể xác nhận kiểu tại thời gian biên dịch bằng cách sử dụng một bộ bảo vệ kiểu do người dùng định nghĩa trả về true (kiểu trả về được chú thích là một "predicate kiểu" dưới dạng arg is T) nếu hình dạng phù hợp với mong đợi của bạn:

interface A {
  foo: string;
}

interface B {
  bar: number;
}

function isA(obj: any): obj is A {
  return obj.foo !== undefined 
}

function isB(obj: any): obj is B {
  return obj.bar !== undefined 
}

function func(obj: any) {
  if (isA(obj)) {
    // In this block 'obj' is narrowed to type 'A'
    obj.foo;
  }
  else if (isB(obj)) {
    // In this block 'obj' is narrowed to type 'B'
    obj.bar;
  }
}

Ví dụ trong Sân chơi

Mức độ bạn thực hiện triển khai type-guard như thế nào thực sự phụ thuộc vào bạn, nó chỉ cần trả về true hoặc false. Ví dụ, như Carl đã chỉ ra trong câu trả lời của mình, ví dụ trên chỉ kiểm tra rằng các thuộc tính mong đợi đã được định nghĩa (theo ví dụ trong tài liệu), chứ không kiểm tra rằng chúng được gán kiểu mong đợi. Điều này có thể trở nên phức tạp với các kiểu nullable và các đối tượng lồng nhau, việc xác định chi tiết đến mức nào để kiểm tra hình dạng là tùy thuộc vào bạn.

Câu trả lời #2

Mở rộng câu trả lời của Aaron, tôi đã tạo ra một transformer mà sinh các hàm bảo vệ kiểu tại thời gian biên dịch. Bằng cách này, bạn không phải tự viết chúng thủ công.

Ví dụ:

import { is } from 'typescript-is';

interface A {
  foo: string;
}

interface B {
  bar: number;
}

if (is<A>(obj)) {
  // obj is narrowed to type A
}

if (is<B>(obj)) {
  // obj is narrowed to type B
}

Bạn có thể tìm thấy dự án tại đây, kèm theo hướng dẫn sử dụng:

https://github.com/woutervh-/typescript-is

Câu trả lời #3

"Tôi đang cố gắng tìm cách truyền một đối tượng vào hàm và kiểm tra kiểu của nó trong thời gian chạy".

Vì một thể hiện của lớp chỉ là một object, câu trả lời “gốc” là sử dụng một thể hiện của lớp và instanceof khi cần kiểm tra kiểu tại runtime, sử dụng một interface khi không cần để giữ hợp đồng và tách rời ứng dụng của bạn, giảm kích thước chữ ký trên các phương thức/ctor, trong khi không thêm bất kỳ kích thước nào. Theo ý kiến khiêm tốn của tôi, đây là một trong số những cân nhắc chính đối với tôi trong TypeScript khi quyết định sử dụng lớp hay type/interface. Một yếu tố quan trọng khác là liệu đối tượng đó có bao giờ cần được khởi tạo hay không so với việc nó chỉ định nghĩa một POJO.

Trong cơ sở mã của tôi, tôi thường có một lớp thực hiện một giao diện và giao diện được sử dụng trong quá trình biên dịch để đảm bảo an toàn kiểu vào thời gian trước biên dịch, trong khi các lớp được sử dụng để tổ chức mã của tôi và cho phép dễ dàng truyền dữ liệu giữa các hàm, lớp và phương thức cũng như thực hiện kiểm tra kiểu thời gian chạy trong TypeScript.

Hoạt động vì routerEvent là một thể hiện của lớp NavigationStart

if (routerEvent instanceof NavigationStart) {
  this.loading = true;
}

if (routerEvent instanceof NavigationEnd ||
  routerEvent instanceof NavigationCancel ||
  routerEvent instanceof NavigationError) {
  this.loading = false;
}

Sẽ không hoạt động

// Must use a class not an interface
export interface IRouterEvent { ... }
// Fails
expect(IRouterEvent instanceof NavigationCancel).toBe(true); 

Sẽ không hoạt động

// Must use a class not a type
export type RouterEvent { ... }
// Fails
expect(IRouterEvent instanceof NavigationCancel).toBe(true); 

Như bạn có thể thấy qua đoạn mã trên, các lớp được sử dụng để so sánh thể hiện với các kiểu NavigationStart|Cancel|Error trong thư viện Angular và nếu bạn đã từng sử dụng router trước đây trong một dự án, tôi tin rằng bạn đã thực hiện các kiểm tra tương tự nếu không muốn nói là giống hệt trong cơ sở mã của riêng bạn để xác định trạng thái ứng dụng trong thời gian chạy.

Sử dụng instanceof trên một Kiểu hoặc Giao diện là không thể, vì trình biên dịch ts loại bỏ các thuộc tính này trong quá trình biên dịch và trước khi được JIT hoặc AOT diễn giải. Các lớp là một cách tuyệt vời để tạo một kiểu có thể được sử dụng trước khi biên dịch cũng như trong thời gian chạy JS.

Cập nhật 2022

Ngoài phản hồi ban đầu của tôi về việc này, bạn có thể tận dụng API Reflect Metadata của TypeScript hoặc tự triển khai giải pháp của riêng bạn bằng cách sử dụng trình biên dịch TypeScript để thực hiện phân tích tĩnh mã của bạn và phân tích AST, truy vấn như sau:

switch (node.kind) {
  case ts.SyntaxKind.InterfaceDeclaration:
    // ...
    break;
  case ts.SyntaxKind.TypeDeclaration:
    // ...
    break;
}

Xem giải pháp này để biết thêm chi tiết

Bài viết liên quan