I am using a function that returns a type predicate to tell TypeScript that a source definitively does or doesn't have a media
property. If it is a SourceWithoutMedia
, it should be handled one way, and if it's a SourceWithMedia
, it should be handled a different way.
The problem is what TypeScript infers from the case where the type predicate returns false.
I would expect TypeScript to infer that if isSourceWithoutMedia(source)
returns false, then source
is a SourceWithMedia
.
What it's actually inferring is that if isSourceWithoutMedia(source)
returns false, then the type of source
is never
.
What am I missing here, and is there a way to get TypeScript to infer that if isSourceWithoutMedia(source)
returns false, then source
is a SourceWithMedia
?
interface SourceWithMedia {
id: string
someOtherProperty: string
media?: string
}
interface SourceWithoutMedia {
id: string
}
const isSourceWithoutMedia = (source: SourceWithMedia | SourceWithoutMedia): source is SourceWithoutMedia => {
return Object.keys(source).length === 1
}
function doSomethingWithMedia (source: SourceWithMedia | SourceWithoutMedia) {
if (isSourceWithoutMedia(source)) return source
if (source.media) return source.media // Error: Property 'media' does not exist on type 'never'.
}
...if isSourceWithoutMedia(source) returns false, then source is a SourceWithMedia?
No.
The problem is that SourceWithMedia
extends SourceWithoutMedia
i. e. SourceWithMedia
is a subtype of SourceWithoutMedia
.
const withMedia: SourceWithMedia = {id: "", someOtherProperty: "", media: ""}
const withoutMedia = withMedia; // valid
When trying to narrow source
to SourceWithoutMedia
using that predicate actually nothing happens. source
will still have a union type, even if you use something like !("media" in source)
in your predicate or check for a missing media
parameter first like so:
function doSomethingWithMedia(source: SourceWithMedia | SourceWithoutMedia) {
if (isSourceWithoutMedia(source)) {
source;
//^? (parameter) source: SourceWithMedia | SourceWithoutMedia
}
}
Now, if we follow the negative branch of the if
statement there aren't any union members left to narrow. TypeScript represents empty unions with the never
type.
On the other hand, if you invert your predicate logic and check whether the media
property is there explicitly, you'll be able to narrow source
to SourceWithMedia
. That way TypeScript can actually distinguish between the two types and tell that we're dealing with a value of type SourceWithMedia
because SourceWithoutMedia
is not assignable to SourceWithMedia
.