Search code examples
typescripttype-inference

TypeScript is inferring a never type when a type predicate function returns false


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'.
    
}

Example on TypeScript playground


Solution

  • ...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.

    TypeScript Playground