如何在TypeScript中运行时检查对象类型?

110

我试图找到一种方法,在运行时将一个对象传递给函数并检查其类型。以下是伪代码:

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);

但是typeof总是返回"object",我找不到一种方法来获取ab的真实类型。 instanceof也不起作用并返回相同的结果。

有任何想法如何在TypeScript中实现吗?

10个回答

144

编辑: 我想指出,从搜索中来到这里的人们需要注意,这个问题特别处理非类类型,即由interfacetype别名定义的对象形状。对于类类型,您可以使用JavaScript的instanceof来确定实例所属的类,TypeScript将自动缩小类型检查器中的类型。

类型在编译时被剥离,不会在运行时存在,因此您无法在运行时检查类型。

您可以做的是检查对象的形状是否符合您的期望,并且TypeScript可以使用用户定义的类型保护在编译时断言类型,如果形状与您的期望匹配,则返回true(注释返回类型是一个形式为arg is T的“类型谓词”):

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;
  }
}

在 Playground 中的示例

你需要决定类型守卫实现的深度,它只需返回 true 或 false。例如,正如 Carl 在他的回答中指出的那样,上面的示例仅检查是否定义了期望的属性(遵循文档中的示例),而不是它们被分配了期望的类型。这可能会在可为空类型和嵌套对象中变得麻烦,你需要确定形状检查的详细程度。


请查看此链接:https://aliolicode.com/2016/04/23/type-checking-typescript/。请确保您也看到了这一行:console.log(john instanceof Person); // true...干杯! - peter70
1
@peter70 这只适用于类实例,而不适用于其他类型(如接口)。OP提到 instanceof 不起作用,所以我猜他有一个非类实例对象。 - Aaron Beall
当将类型信息传输到运行时代码中时,这是可能的。例如,在TS> 2.4中使用自定义转换器。 - Christian
唉,我希望有一种更优雅、更简洁的方法来做这件事,比如在if语句本身中断言类型,而不需要所有这些辅助函数。 - 55 Cancri
1
为什么 TypeScript 不能允许 if (isA(obj)): obj is A { ... } - 55 Cancri

35

在Aaron的回答基础上,我创建了一个可以在编译时生成类型守卫函数的转换器。这样,您就不必手动编写它们。

例如:

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
}

您可以在此处找到该项目,并获得使用说明:

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


9
"typescript-is" 不好用。它强制我使用 "ttypescript"。 - ian park
1
@ianpark,实际上你不必强制使用 ttypescript,你也可以使用 typescript API 编译你的项目,并自己配置转换器。ttypescript推荐的方式,因为它会为你完成这些工作。当你使用转换器时,目前没有其他选择。而且,ttypescript 有什么问题呢?;-) - user7132587
是的,你说得对。ttypescript 是推荐的方式,也是一个很好的解决方案。我的观点是,对于那些不想添加另一个编译器的人来说,这是一个不错的选择。如果你使用 typescript-is,你需要添加另一个编译器或编写自己的编译逻辑。这将增加另一种复杂性。 - ian park

12
我正在尝试找到一种方法,在运行时将一个对象传递给函数并检查它的类型。由于类实例只是一个“对象”,因此使用类实例和instanceof在需要运行时类型检查时使用“本机”答案,使用接口而不是为了保持合同并解耦您的应用程序,减少方法/构造函数的签名大小,同时不添加任何额外的大小。在我看来,这是在TypeScript中决定使用类与类型/接口之间考虑的几个主要因素之一。另一个主要驱动因素是对象是否需要被实例化,例如是否定义了POJO。
在我的代码库中,我通常会有一个实现接口的类,并且接口在编译期间用于预编译时间类型安全性,而类则用于组织我的代码,并允许在TypeScript中在函数、类和方法之间传递数据以及进行运行时类型检查。 这起作用是因为routerEvent是NavigationStart类的一个实例
if (routerEvent instanceof NavigationStart) {
  this.loading = true;
}

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

无法正常工作

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

无法运行

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

正如您在上面的代码中所看到的,类用于将实例与Angular库中的NavigationStart|Cancel|Error类型进行比较。如果您之前使用过路由器,在运行时检查应用程序状态时,很可能在自己的代码库中执行了类似甚至相同的检查。
使用instanceof来检查类型或接口是不可能的,因为ts编译器在其编译过程中会剥离这些属性,在被JIT或AOT解释之前。类是一种很好的方式,可以在预编译期间以及在JS运行时期间创建一个类型。 2022年更新 除了我最初对此的回答外,您还可以利用TypeScript反射元数据API或使用TypeScript编译器自行解决方案,对代码进行静态分析并解析AST,进行查询,如下所示:
switch (node.kind) {
  case ts.SyntaxKind.InterfaceDeclaration:
    // ...
    break;
  case ts.SyntaxKind.TypeDeclaration:
    // ...
    break;
}

请参考此解决方案获取更多详细信息。

3

我一直在尝试Aaron的答案,认为最好是使用typeof进行测试,而不仅仅是undefined,像这样:

interface A {
  foo: string;
}

interface B {
  bar: number;
}

function isA(obj: any): obj is A {
  return typeof obj.foo === 'string' 
}

function isB(obj: any): obj is B {
  return typeof obj.bar === 'number' 
}

function func(obj: any) {
  if (isA(obj)) {
    console.log("A.foo:", obj.foo);
  }
  else if (isB(obj)) {
    console.log("B.bar:", obj.bar);
  }
  else {console.log("neither A nor B")}
}

const a: A = { foo: 567 }; // notice i am giving it a number, not a string 
const b: B = { bar: 123 };

func(a);  // neither A nor B
func(b);  // B.bar: 123

1
你应该使用单独的动态类型库来定义具有动态类型信息的自定义类型,并跟踪其与预期类型的兼容性。
可以使用这个神奇的库,它允许你这样做:https://github.com/pelotom/runtypes 使用它,你可以为你的A和B类型定义一个元类型:
const AType = Record({foo: Number})
const BType = Record({baz: String})

这是纯TS代码,可以注意到我们正在创建常量对象,而不是静态类型。此外,我们正在使用库提供的Number和String对象,而不是TS的静态类型number和string。

然后,您需要创建A和B的静态类型声明:

type A = Static<typeof AType>
type B = Static<typeof BType>

现在,这些类型是合适的 Typescript 静态类型。它们包含了您在创建元类型时传递的所有正确成员,直到对象的无限深度。支持数组、对象、可选值、虚假值和标量类型。

然后,您可以像这样使用它:

function asdf(object: any): A | undefined {
    try {
        const aObject = AType.check(object) // returns A if complies with Record definition, throws otherwise
        return aObject
    } catch {
        return undefined
    }
}

asdf({ foo: 3 }) // returns A, literally the same object since it passed the check
asdf({ bar: "3" }) // returns undefined, because no `foo` of type `number`
asdf({ foo: "3" }) // returns undefined, because `foo` has wrong type

这是最现代化、最严谨的解决方案,能够出色地工作和扩展。

1
你应该使用“in”运算符进行缩小范围。 参考文献
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}

1
我知道这是一个老问题,而且这里的“真正”问题与标题中的问题不同,但谷歌会将此问题抛出来以获取“typescript runtime types”,有些人可能知道他们正在寻找什么,它可以是运行时类型。正确答案是Aaron Beall所回答的类型保护。但是,匹配标题问题和匹配谷歌搜索的答案只有TypeScript转换器/插件的使用。TypeScript本身在将TS转换为JS时会剥离类型信息。嗯,这是实现类型保护的可能方式之一,例如,如user7132587所指出的typescript-is转换器。另一个选择是转换器tst-reflect。它提供了有关类型的所有信息,并允许您基于类型信息编写自己的类型保护,例如检查对象是否具有您期望的所有属性。或者,您可以直接使用转换器中基于TypeScript类型检查信息的Type.is(Type)方法。
我已经创建了this REPL。玩得开心!更多信息请参见Github repository
import { getType, Type } from "tst-reflect";

class A {
  alphaProperty: string;
}

interface B {
  betaProperty: string;
}

class Bb extends A implements B {
  betaProperty = "tst-reflect!!";
  bBetaProperty: "yes" | "no" = "yes";
}

/** @reflectGeneric */
function func<TType>(obj?: TType) 
{
    const type: Type = getType<TType>();

    console.log(
      type.name, 
      "\n\textends", type.baseType.name,
      "\n\timplements", type.getInterface()?.name ?? "nothing",
      "\n\tproperties:", type.getProperties().map(p => p.name + ": " + p.type.name),
      "\n"
    );
    
    console.log("\tis A:", type.is(getType<A>()) ? "yes" : "no");
    console.log("\tis assignable to A:", type.isAssignableTo(getType<A>()) ? "yes" : "no");
    console.log("\tis assignable to B:", type.isAssignableTo(getType<B>()) ? "yes" : "no");
}

let a: A = new A();
let b: B = new Bb();
let c = new Bb();

func(a);
func<typeof b>();
func<Bb>();

输出:

A 
    extends Object 
    implements nothing 
    properties: [ 'alphaProperty: string' ] 

    is A: yes
    is assignable to A: yes
    is assignable to B: no
B 
    extends Object 
    implements nothing 
    properties: [ 'betaProperty: string' ] 

    is A: no
    is assignable to A: no
    is assignable to B: yes
Bb 
    extends A 
    implements B 
    properties: [ 'betaProperty: string', 'bBetaProperty: ' ] 

    is A: no
    is assignable to A: yes
    is assignable to B: yes

不确定为什么这个被踩了,tst-reflect是一个很好的解决方案。或者使用typescript-rtti :-P - William Lahti

1
你可以调用构造函数并获取它的名称 let className = this.constructor.name

3
值得注意的是,不建议使用 constructor.name,因为 JavaScript 压缩器会忽略此属性,并用其生成的名称覆盖它。 - Michael Fulton

1

,你不能在运行时引用一个type,但是是的,你可以使用typeof将一个object转换为type,并在运行时对这个object进行验证/清理/检查。

const plainObject = {
  someKey: "string",
  someKey2: 1,
};
type TypeWithAllOptionalFields = Partial<typeof plainObject>; //do further utility typings as you please, Partial being one of examples.

function customChecks(userInput: any) {
  // do whatever you want with the 'plainObject'
}


以上等同于
type TypeWithAllOptionalFields = {
  someKey?: string;
  someKey2?: number;
};
const plainObject = {
  someKey: "string",
  someKey2: 1,
};
function customChecks(userInput: any) {
  // ...
}


但是在您的代码中不要重复使用关键字名称。

-1

无需检查类型的替代方法

如果您想引入更多类型,该怎么办?那么您会扩展if语句吗?您的代码库中有多少这样的if语句?

在条件中使用类型会使您的代码难以维护。有很多理论支持这一点,但我会为您省去这些麻烦。以下是您可以采取的替代方法:

使用多态

像这样:

abstract class BaseClass {
    abstract theLogic();
}

class A extends BaseClass {
    theLogic() {
       // do something if class is A
    }
}


class B extends BaseClass {
    theLogic() {
       // do something if class is B
    }
}

然后你只需要从任何你想要的类中调用theLogic()方法:

let a: A = new A();
a.theLogic();

let b: B = new B();
b.theLogic();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接