How to check the object type on runtime in TypeScript?
Asked Answered
M

10

115

I'm trying to find a way to pass an object to function in and check it type in a runtime. This is a pseudo code:

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

But typeof always returns "object" and I could not find a way to get the real type of a or b. instanceof did not work either and returned the same.

Any idea how to do it in TypeScript?

Mckinleymckinney answered 19/5, 2017 at 20:2 Comment(0)
M
148

Edit: I want to point out to people coming here from searches that this question is specifically dealing with non-class types, ie object shapes as defined by interface or type alias. For class types you can use JavaScript's instanceof to determine the class an instance comes from, and TypeScript will narrow the type in the type-checker automatically.

Types are stripped away at compile-time and do not exist at runtime, so you can't check the type at runtime.

What you can do is check that the shape of an object is what you expect, and TypeScript can assert the type at compile time using a user-defined type guard that returns true (annotated return type is a "type predicate" of the form arg is T) if the shape matches your expectation:

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

Example in Playground

How deep you take the type-guard implementation is really up to you, it only needs to return true or false. For example, as Carl points out in his answer, the above example only checks that expected properties are defined (following the example in the docs), not that they are assigned the expected type. This can get tricky with nullable types and nested objects, it's up to you to determine how detailed to make the shape check.

Metal answered 19/5, 2017 at 20:32 Comment(5)
check this: aliolicode.com/2016/04/23/type-checking-typescript Please be shore, you see this line also: console.log(john instanceof Person); // true ...cheers!Leticialetisha
@Leticialetisha That only works with class instances, not other types (like interfaces). OP mentioned instanceof didn't work so I assume he has a non-class instance object.Metal
It is possible when transporting type information into runtime code. E.g. with a custom transformer in TS > 2.4.Woofer
Ugh, I wish there were a more elegant, less verbose way to do this, such as being able to assert the type in the if statement itself without the need for all these helper functions.Frawley
Why cant typescript allow if (isA(obj)): obj is A { ... }?Frawley
H
35

Expanding on Aaron's answer, I've made a transformer that generates the type guard functions at compile time. This way you don't have to manually write them.

For example:

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
}

You can find the project here, with instructions to use it:

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

Heroworship answered 19/9, 2018 at 7:21 Comment(5)
typescript-is is not good. they force me to use ttypescript.Mcdaniels
@ianpark actually you are not forced to use ttypescript, you can also programmatically compile your project with the typescript API and configure the transformers yourself. ttypescript is the recommended way because it does this for you. When you use a transformer you have no other choice at the moment. And what's wrong with ttypescript anyway? ;-)Heroworship
yes. you're right. ttypescript is recommended way and it is good solution. My opinion is for whom do not want to add another compiler. If you use typescript-is, you should add another compiler or write own compile logic. It will become another complexity.Mcdaniels
Can it handle type aliases as well?Taipan
2024: typescript-is author deprecated the package in favour of typia which seems to be adopted quite well.Launcher
G
13

"I'm trying to find a way to pass an object to function in and check it type in a runtime".

Since a class instance is just an object, the "native" answer is to use a class instance and instanceof when runtime type checking is needed, use an interface when not in order to keep a contract and decouple your application, make reduce signature size on methods/ctors, while not add any additional size. In my humble opinion this is one of a few main considerations for me in TypeScript when deciding to use a class vs type/interface. One other main driving factor is whether the object will ever need to be instantiated vs if it for instance defines a POJO.

In my codebase, I will typically have a class which implements an interface and the interface is used during compilation for pre-compile time type safety, while classes are used to organize my code and allow for ease in passing data between functions, classes and method as well as do runtime type checks in typescript.

Works because routerEvent is an instance of NavigationStart class

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

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

Will not work

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

Will not work

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

As you can see by the code above, classes are used to compare the instance to the types NavigationStart|Cancel|Error within the Angular library and if you have used the router before you a project I am willing to be that you have done similar if not identical checks within your own codebase in order to determine application state during runtime.

Using instanceof on a Type or Interface is not possible, since the ts compiler strips away these attributes during its compilation process and prior to being interpreted by JIT or AOT. Classes are a great way to create a type which can be used pre-compilation as well as during the JS runtime.

Update 2022

In addition to my original response to this, you can leverage The TypeScript Reflect Metadata API or roll your own solution using the TypeScript compiler to do static analysis of your code and parse the AST, querying like so:

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

See this solution for additonal details

Grandmother answered 22/2, 2019 at 3:33 Comment(0)
P
3

I've been playing around with the answer from Aaron and think it would be better to test for typeof instead of just undefined, like this:

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
Proteinase answered 9/9, 2018 at 3:12 Comment(0)
S
2

You can call the constructor and get its name

let className = this.constructor.name

Scandium answered 10/3, 2021 at 15:32 Comment(1)
It's worth noting using contructor.name is not recommended as Javascript minifiers ignore this property and override it with their generated names.Viewy
C
2

You should use a separate dynamic typing library that allows you to define custom types with dynamic type information and track it's compliance to the expected type.

The best solution that allows you this is this amazing library: https://github.com/pelotom/runtypes

Using it, you can define a meta-type for your A and B types:

const AType = Record({foo: Number})
const BType = Record({baz: String})

This is pure TS, and you can note that we are creating constant objects, not static types. Also, we are using Number and String objects provided by the library, and not the TS static types of number and string.

Then, you create the static type declarations for A and B:

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

Now, these types are proper Typescript static types. They contain all the proper members that you passed during the creation of the meta-type, up to the infinite depth of objects. Arrays, objects, optionals, faulsy values, scalar types are all supported.

Then you can use this like this:

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

This is the most modern, serious solution that works and scales beautifully.

Computation answered 14/6, 2022 at 22:5 Comment(0)
V
1

No, You cannot reference a type in runtime, but yes you can convert an object to a type with typeof, and do validation/sanitisation/checks against this object in runtime.

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

Above is equal as

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

but without duplication of keynames in your code

Vaden answered 28/5, 2020 at 4:21 Comment(0)
M
1

I know this is an old question and the "true" question here is not the same as the question in the title, but Google throws this question for "typescript runtime types" and some people may know what they are looking for and it can be runtime types.

Right answer here is what Aaron Beall answered, the type guards.

But answer, matching question in the title and matching Google searches, is only usage of TypeScript transformers/plugins. TypeScript itself strip out information about types when transpiling TS to JS. And well,.. it is one of the possible ways how to implement the type guards, eg. the typescript-is transformer as user7132587 pointed out.

Another options is transformer tst-reflect. It provides all the information about types at runtime. It allows you to write own type guards based on type information, eg. checking that object has all the properties you expect. Or you can use directly Type.is(Type) method from the transformer, which is based directly on the TypeScript's type checking information.

I've created this REPL. Have a fun! More info in 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>();

Output:

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
Michelle answered 18/10, 2021 at 17:26 Comment(1)
Not sure why this got downvoted, tst-reflect is a good solution for this. That or typescript-rtti :-PTransect
S
1

You should use the "in" operator to narrow down. Reference

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
 
  return animal.fly();
}
Stays answered 7/10, 2022 at 15:49 Comment(0)
C
-1

Alternative approach without the need of checking the type

What if you want to introduce more types? Would you then extend your if-statement? How many such if-statements do you have in your codebase?

Using types in conditions makes your code difficult to maintain. There's lots of theory behind that, but I'll save you the hazzle. Here's what you could do instead:

Use polymorphism

Like this:

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

Then you just have to invoke theLogic() from whichever class you want:

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

let b: B = new B();
b.theLogic();
Capriole answered 30/5, 2020 at 11:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.