Bart van den Burg
Bart van den Burg

Reputation: 2364

Typescript: type determination based on property value

I was pretty sure Typescript was able to determine an extended class based on a property value, e.g.:

interface Base {
    type: string;
    child?: Base;
}

interface Ext extends Base {
    type: 'test';
    sth: string;
}

z({
    type: 'a',
    child: {
        type: 'b',
    }
}); // ok

z({
    type: 'a',
    child: {
        type: 'test',
        sth: 'val'
    }
}); // not ok

function z(input: Base) { }

Above example doesn't work, TS tells me property sth doesn't exist on interface Base. What do I need to change so that TS will understand the child to actually be type of Ext, because of the value 'test' on the type property?

Upvotes: 0

Views: 1029

Answers (3)

Bart van den Burg
Bart van den Burg

Reputation: 2364

I think I figured it out:

interface Base {
    child?: Ext;
}

interface Ext1 extends Base {
    type: 'a';
}
interface Ext2 extends Base {
    type: 'test';
    sth: string;
}

type Ext = Ext1 | Ext2;

z({
    type: 'a',
    child: {
        type: 'test',
        sth: 'x'
    }
});

function z(input: Ext) { }

This example will fail if sth is not defined while type is 'test' instead of the other way around

Upvotes: 2

artem
artem

Reputation: 51769

This error comes from excess property check, which is done only when variable is initialized with object literal. To avoid it, you need to initialize the value with something which is not an object literal, for example you can add intermediate variable

let o = {
    type: 'test',
    sth: 'value'
}
let x1: Base = o;

or you can add a type assertion

let x2: Base = {
    type: 'test',
    sth: 'value'
} as Base;

Another solution is to make Base and z generic, parameterized on the type of Child which should be a subtype of Base (note that self-referential type constraints are tricky to get right, but it seems to work in this case, the minor issue is with default value for Base which causes Base in the constraint to be inferred as Child extends Base<{ type: string; child?: Base<any> | undefined; } - any here could be problematic but does not seem to affect anything in the example).

interface Base<Child extends Base = { type: string, child?: Base }> {
    type: string;
    child?: Child;
}

interface Ext extends Base {
    type: 'test';
    sth: string;
}

z({
    type: 'a',
    child: {
        type: 'b',
    }
}); // ok

z({
    type: 'a',
    child: {
        type: 'test',
        sth: 'val'
    }
}); // not ok

function z<B extends Base>(input: B) { }

Upvotes: 0

Naftali
Naftali

Reputation: 146350

You need to declare it as type Ext and it should pass

let x: Ext = {
    type: 'test',
    sth: 'value'
}

Upvotes: 1

Related Questions