Home > Enterprise >  Infer class generics when using `instanceof`
Infer class generics when using `instanceof`

Time:12-01

Assuming we have the following code:

class Foo<T = number> {
  foo: T;

  constructor(foo: T) {
    this.foo = foo;
  }
}

const F: typeof Foo<number> = Foo;

let f: unknown;

if (f instanceof F) {
  f.foo; // 'any', why is this not a 'number'?
}

Playground

Why is f of type Foo<any> and not Foo<number>? And can I make this work only using instanceof or do I have to use a type predicates function?

CodePudding user response:

You could use typeguards e.g

function isF(isF: unknown): isF is Foo<number>{
  return isF instanceof F;
}

if (isF(f)) {
  f.foo; // Foo<number>
}

CodePudding user response:

It is not safe to assume that f is of type Foo<number> by checking f instanceof F. The instanceof operator does not care about generics as they are erased at runtime.

We can instantiate f with new Foo("a string") without an issue. f instance F will return true, even though f is of type Foo<string>.

const F: typeof Foo<number> = Foo;

let f: unknown = new Foo("a string")

console.log(f instanceof F)
// true

if (f instanceof F) {
  console.log(f.foo);
  // "a string"
}

The only safe way to check if f is Foo<number> is to use a type guard which both checks if f is instanceof Foo and typeof f.foo === "number".

const isFooNumber = (arg: unknown): arg is Foo<number> => {
  return arg instanceof Foo && typeof arg.foo === "number"
}

if (isFooNumber(f)) {
  console.log(f.foo);
}

Playground

CodePudding user response:

As @TobiasS. and @MuratKaragöz pointed out, the instanceof operator can not distinguish Foo and F at runtime, hence the generics are not passed down.

There are two paths to fixing this:

Using instanceof only

Unsafe at runtime

Instead of typing F with typeof Foo<number> as I did. We can create a new constructor type which return an instance of Foo<number>:

class Foo<T = number> {
  foo: T;

  constructor(foo: T) {
    this.foo = foo;
  }
}

const F: new (...args: ConstructorParameters<typeof Foo<number>>) => Foo<number> = Foo

let f: unknown = new F(0)

if (f instanceof F) {
  f.foo; // number
}

This is a very simple way of typing F, but it IS unsafe at runtime because it tricks Typescript into thinking Foo and F are not the same class, even thought (new Foo()) instanceof F returns true at runtime (see the safer method below).

Although, this trick can still be useful in few niche cases (I personally ended up using a combination of the safe and unsafe methods).

Safe at runtime

This is most likely the preferred solution, as it does not suffer from the same caveat as the unsafe solution above.

Put simply, we can create a new class F which extends Foo<number>:

class Foo<T = number> {
  foo: T;

  constructor(foo: T) {
    this.foo = foo;
  }
}

class F extends Foo<number> {
  constructor(...args: ConstructorParameters<typeof Foo<number>>) {
    super(...args)
  }
}

let f: unknown;

if (f instanceof F) {
  f.foo; // number
}

This IS safer because (new Foo()) instanceof F will return false at runtime, on the opposite of the unsafe solution.

Using type guards

See @TobiasS.'s and @MuratKaragöz's answers.

  • Related