Typescript's conditional : T extends U ? X : Y
syntax is pretty powerful, but I have not found a way to specify the type of a return based on an attribute of a class, or something in the function itself. For instance, take this simple class:
class X {
members: number[] = [];
returnRoman = false;
first(): number|string {
if (!this.returnRoman) {
return members[0];
} else {
return numberToRomanNumeral(members[0]);
}
}
}
Is there a way to narrow the type returned to simply number or string for type-checking? The particular issue I have is that I have a structure like this:
class Base {
x: number = 0;
}
class Sub extends Base {
y: number = 0;
}
class FilterIterator {
members: Base[] = [];
filterClass: typeof Base = undefined;
class first(): Base {
for (const m of this.members) {
if (this.filterClass !== undefined && !(m instanceof this.filterClass)) {
continue;
}
return m;
}
}
}
const fi = FilterIterator();
fi.filterClass = Sub
fi.members = [new Sub(), new Sub()];
const s = fi.first();
if (s.y > 0) {
console.log('s is moving up in the world!');
}
Because fi.first()
is typed to return Base
, the line if (s.y > 0)
raises errors because y
does not exist on Base. In this case, it wouldn't be too hard to do:
const s = fi.first() as Sub;
but if we had something like this instead:
class FilterIterator extends Base {
...
filterClass = FilterIterator; // I know, not possible simply, but there are ways...
}
then you end up with code such as:
const fi = FilterIterator();
[ set up .members with nested FilterIterators ]
const firstOfFirstOfFirst = fi.first().first().first();
being rewritten as:
const firstOfFirstOfFirst = (((fi.first() as FilterIterator).first()
as FilterIterator).first() as FilterIterator);
and it gets even worse if we actually create generators that can return generators, etc. since declaring types within for...of
loops is not yet possible. It also moves the logic of determining what the value returned should be to the consuming software where FilterIterator or Base itself should know what classes might be returned.
Any solutions or improvements would be welcome.
Your simplified example is harder to type properly because
returnRoman
is an instance variable that can presumable change its value multiple times within the same instance. You would have to update it via a setter and use the syntaxasserts this is this & SomeType
.Your actual use case could potentially run into the same difficulties of needing to convert a type after it has already been set, but we can circumvent those issues by taking the
filterClass
as an argument in the constructor. This also means that the type of thefilterClass
can always be properly inferred for an instance because it is known from the moment that the instance is created. So it becomes a straight-forward use case for generic classes.instanceof
is an automatic type guard in typescript, so if you hit the line withreturn m
typescript knows thatm
is guaranteed to be the same typeT
of yourfilterClass
.I think you are trying to do something a little different, where the filter is some sort of custom check rather than a class constructor, but I don't have enough information to really type that properly. You would define your filter as a user-defined type guard.
Typescript Playground Link