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'?
}
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);
}
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.