type DequeNode<T> = {
value: T;
prev?: DequeNode<T>;
next?: DequeNode<T>;
};
class Deque<T> {
head?: DequeNode<T>;
tail?: DequeNode<T>;
size = 0;
first() {
return this.head?.value;
}
isEmpty() {
return this.head == null
}
}
const deque = new Deque<number>()
if (!deque.isEmpty()) {
deque.first() // number | undefined
deque.head?.value // number | undefined
}
if (deque.head) {
deque.first() // number | undefined
deque.head.value // number
}
In the above code, it can correctly infer type of deque.head.value if deque.head is not null. But it can't work when using deque.first()
How can I make isEmpty() check work and make first() return only type number?
The only way checking
deque.isEmpty()would work to narrow the apparent type ofdequeis if it were a user-defined type guard method with a return type of the formthis is ⋯. You could give a name to the type of aDeque<T>for whichisEmpty()istrue:and then make
isEmpty()a type guard method:But while this does what you want when you check
deque.isEmpty()directly:It will not behave as expected when you check
!deque.isEmpty():And that's because TypeScript has no built-in mechanism to say that a
Deque<T>which is not anEmptyDeque<T>has any special characteristics. You'd like to say that whenisEmpty()returnsfalsethat it narrowsDeque<T>to aNonEmptyDeque<T>defined likebut there's no syntax for describing the
falseside of a type guard. There's a longstanding feature request for it at microsoft/TypeScript#15048, and if that is ever implemented maybe you could writeand everything would behave as you want. But you can't do that, so we're stuck.
Ideally you'd like to say that a
Deque<T>is either anEmptyDeque<T>or aNonEmptyDeque<T>, meaning that conceptually it is a union type likeIf that were the case, then when you narrow a
Deque<T>to prohibitEmptyDeque<T>with!deque.isEmpty(), then it would become aNonEmptyDeque<T>necessarily.But classes in TypeScript cannot be unions; or at least class declarations do not result in union-typed instances. You can describe the type of the constructor like
but any such assignment will confuse the compiler, since the class constructor expression on the righthand side will not be seen as constructing a union. You'll need to use a type assertion like
and deal with the laxness in compiler verification by being very careful.
So all that gives us
which we can now test:
This looks good and works as you want. But is it worth it?
It really depends on your use case whether something like this is worthwhile. Personally if I needed to distinguish between these two cases at the type level and still use classes, I'd just write a
BaseDeque<T>class and then haveNonEmptyDeque<T>andEmptyDeque<T>subclasses, and use the union typeDeque<T>elsewhere. This prevents the need for type assertions. But if we're refactoring like this we might as well avoid classes entirely since you could get by with plain objects also. The particular pros and cons of this are out of scope here, so I'll stop digressing.Playground link to code