Contrary to common belief, the type of a class instance is not determined by its contextual typing. When a class declares an extends
or implements
relationship, it only conducts a post-facto check against the superclass or interface without influencing the actual type. Failure to adhere to the declared types results in a compiler warning, but in terms of actual types, removing the extends
or implements
clause would yield the same result. This aspect of TypeScript is generally unpopular.
The issue was raised in microsoft/TypeScript#3667, with an unsuccessful attempt to address it in microsoft/TypeScript#6118, which was eventually abandoned due to compatibility issues with existing libraries; refer to this comment. There are ongoing discussions on this topic, such as microsoft/TypeScript#32082; showing support on that thread may lead to improvements. However, for now, we must accept that
class Foo implements Bar {/*...*/}
has similar implications on the type of
Foo
as
class Foo {/*...*/}
.
In practical scenarios,
class FishClass implements Fish {/*...*/}
behaves equivalently to
class FishClass {/*...*/}
concerning member inference. For instance, initializing the
alive
property with a
string
value like
"Yes"
leads the compiler to infer its type as
string
. Subsequently assigning
true
to it results in an error because you cannot assign a
boolean
to a
string
. Notably, while
string
can be assigned to
string | boolean
, the validation of
FishClass implements Fish
allows narrowing properties of subtypes.
To manage this situation, manually specify the desired type for the field:
class FishClass implements Fish {
alive: string | boolean = 'Yes'; // acceptable
constructor() {
this.alive = true; // acceptable
}
}
Link to code execution in TypeScript Playground