How can I optionally embed one or more interfaces inside of a Typescript interface?

Viewed 1021

I want to define a type D that requires all fields of interface A, but optionally also embeds the fields of interface B, C or both.

Here is how I'm trying to define it:

interface A {
    a1: number;
    a2: number;
};

interface B {
    b1: number;
    b2: number;
};

interface C {
    c1: number;
    c2: number;
};

type D = A | (A & B) | (A & C) | (A & B & C);

let d: D;
d = {'a1': 1, 'a2': 2};
if ('c1' in d) {
    d.c1 = 123;
}

However, typescript doesn't seem to like this, and complains: Property 'c1' does not exist on type 'never'.

In case I've explained the above poorly, I want type D to be able to hold the following values:

{"a1": 1, "a2": 2}
{"a1": 1, "a2": 2, "b1": 3, "b2": 4}
{"a1": 1, "a2": 2, "c1": 5, "c2": 6}
{"a1": 1, "a2": 2, "b1": 3, "b2": 4, "c1": 5, "c2": 6}

But not:

{"a1": 1} // missing field a2
{"a1": 1, "a2": 2, "b2": 4} // missing field b1
{"a1": 1, "a2": 2, "c1": 5} // missing field c2
{"a2": 2, "b1": 3, "c2": 6} // missing fields a1, b2, c1

How can I do this in typescript?

3 Answers

If you notice, if you mouse over d on line 3 below, after the assignment on line 2

let d: D;
d = {'a1': 1, 'a2': 2};
if ('c1' in d) { // mouse over `d` here
  d.c1 = 123; // Property 'c1' does not exist on type 'never'.
}

typescript has smartly determined the d is actually type A.

But if you assign it to something that returns D and not a sub type, you will get no error:

let d: D;
d = {'a1': 1, 'a2': 2} as D; // cast
if ('c1' in d) {
  d.c1 = 123; // no error
}

or

function makeD(): D {
  return { a1: 1, a2: 2 };
}

let d: D;
d = makeD();
if ('c1' in d) {
  d.c1 = 123; // no error
}

In other words, typescritp's type awareness goes:

let d: D; // `d` is `D` (possibly unassigned)
d = {'a1': 1, 'a2': 2}; // `d` is assigned something that matches it's sub type `A`, so `d` is `A` after this
if ('c1' in d) { // 'c1' is not in `A` so `d` is never in side this if block
  d.c1 = 123; // Property 'c1' does not exist on type 'never'.
}

so you want to change it to:

let d: D; // `d` is `D` (possibly unassigned)
d = somthingThatIsD; // `d` is assigned (still `D`)
if ('c1' in d) { // 'c1' is in `D` so `d` is still `D`
  d.c1 = 123; // no error
}

one option would be:

interface D extends A, Partial<B>, Partial<C> {}

or if you prefer using type:

type D = A & Partial<B> & Partial<C>;

(see https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgILIN4Chm+XARgC5kQBXAWwCNoBuHPOAJhPOrqwF96tRJZEKAEKYGuKsVKUaUenmRUWU9rK48+0eEmQBhUfISS2MuXgRLjHbll7hNg5ABFkEAB6QQAEwDOaADTIAApwUGDAcAA2ADxCAHwBwaHh0TqxmMicNhEQYMieJI70nsgAvJgA5ITlJAQBlUzVyEzWwDDIABTlhuXIoHkAlPp4ngB0hqXIBEwAzPScQA)

Partial is a built-in thing from TypeScript that takes all of the properties from the passed in interface and makes them optional. This means though that you could have a situation where e.g. c1 is defined, but c2 isn't.

try this

interface A {
    a1: number;
    a2: number;
};

interface B {
    b1: number;
    b2: number;
};

interface C {
    c1: number;
    c2: number;
};

type AB = (A & B);
type AC = (A & C);
type ABC = (A & B & C);
type D = A | AB | AC | ABC;

let d: D;
d = {'a1': 1, 'a2': 2};
if ((d as AC).c1) {
    const ac = (d as AC);
    d = {...d, c1: ac.c1}
}

I think the major feature to leverage on here is the as keyword

Related