Home > other >  How can I make TypeScript infer a return type more "literally"?
How can I make TypeScript infer a return type more "literally"?

Time:11-11

I've been scratching my head and searching the internet for an explanation to this.

I started with my full code and boiled it down to the following that illustrates my problem:

function foo (param: string) {
    if (param === 'bar') {
        return {
            one: 'bar'
        }
    }

    return {
        two: 'baz'
    }
}

Why does this give me:

declare function foo(param: string): {
    one: string;
    two?: undefined;
} | {
    two: string;
    one?: undefined;
};

and not

declare function foo(param: string): {
    one: string;
} | {
    two: string;
};

At first I thought it was a difference between functions and arrow functions, but this issue taught me otherwise and pointed me in the direction that it might be an inference issue.

Then I came across Distributive Conditional Types and thought it might have something to do with it but having read this answer (and hopefully understood it correctly) made me think that it was not.

I have now read a lot of other resources too, but I can't figure out what I'm missing or why I can't put all the pieces together to form an understanding of this.

Playground link

CodePudding user response:

There is no problem here; the type inferred by Typescript here is more useful than the one you propose.

An object type like {one: string} does not mean that an object of that type can only have that one property. This type only means the object must have that property, with no restriction on what other properties the object has. In contrast, a type like {one: string, two?: undefined} means that the object has the property one and does not have the property two, except possibly with the value undefined.

This means that the inferred type is more specific: the object either has one and not two, or it has two and not one. That's more informative than just saying the object either has one or two, because that also means it could have both.


Also note here that when you access an object's properties, Typescript will give you an error if the property is not declared on that type. For example, accessing the property obj.two when obj has type {one: string} | {two: string} will give a type error, and it should: an object of this type could look like {one: 'fish', two: 3.14}, i.e. its two property could be literally anything.

On the other hand, {one: string, two?: undefined} | {one?: undefined, two: string} has the property two in both branches, so there is no type error when you access obj.two. This is because Typescript can know the type of obj.two - it's definitely either string or undefined, it cannot be anything else.

type Foo = {one: string, two?: undefined} | {two: string, one?: undefined}

// error, as desired
let foo: Foo = {one: 'fish', two: 'fish'};

// ok
foo.one;


type Bar = {one: string} | {two: string}

// object is wrong, but no error :-(
let bar: Bar = {one: 'fish', two: 'fish'};

// error when trying to use the object in a sensible way :-(
bar.one;

Playground Link

CodePudding user response:

Actually, the type:

{
    one: string;
    two?: undefined;
} | {
    two: string;
    one?: undefined;
}

is the exact same as*: (Exact Optional Property Types, an optional flag, changes this)

{
    one: string;
} | {
    two: string;
}

since key?: undefined means a non-existent key. The reason why it returns this type is to allow you to do guard checking on the property:

function test(bool: boolean) {
    if (bool)
        return { one: "bar" }
    else
        return { two: "baz" }
}

if (test(true).one) {
    // this wouldn't work otherwise
}

function test2(bool: boolean): { one: string } | { two: string } {
    if (bool)
        return { one: "bar" }
    else
        return { two: "baz" }
}

if (test2(true).one) {
    // as shown!
}

TS Playground

  • Related