Home > front end >  How to properly declare a recursive interface with optional properties of a mapped type
How to properly declare a recursive interface with optional properties of a mapped type

Time:10-31

I am trying to implement an interface of the shape of

type someUnion = "foo" | "bar";

interface RecursiveInterface {
  a: number;
  b: Array<number>;
  [key in someUnion]?: RecursiveInterface;
}

to describe objects like

const someObject: RecursiveInterface = {
  a: 2,
  b: [1,5],
  foo: {
     a: 0,
     b: [2,6],
     bar: {
        a: 8,
        b: 7
     }
  }
}

However this leads to the error A mapped type may not declare properties or methods.

Suggesting that the following is viable

type mappedType = {
  [key in someUnion]?: RecursiveInterface;
}

which throws no errors, but as the initial error suggests, does not allow additional properties.

Extending the initial interface with the mappedType appears to work though, as in

interface RecursiveInterface extends mappedType {
    a: number;
    b: Array<number>
}

Now my question is, is this the correct way to define the desired interface or is there a more concise option, that does not require extending the interface with an additional mapped type?

CodePudding user response:

You could use type and intersect definitions instead:

type RecursiveInterface = {
  a: number;
  b: Array<number>;
} & {
    [key in someUnion]?: RecursiveInterface;
}

It is more "grouped together", if it is what you are after

CodePudding user response:

There is nothing wrong with your definition (except maybe that naming conventions generally use UpperPascalCase for new types, so SomeUnion instead of someUnion and MappedType instead of mappedType, and the type parameter in a mapped type is also a type, not a key name, so K instead of k, but these are just conventions, like coloring the decaf coffee pot orange; not necessary, but helpful to distinguish between different things).

Still, if you want something more concise you can use the Partial<T> and the Record<K, V> utility types so that you can immediately extend a named type instead of having to create a new named type:

type SomeUnion = "foo" | "bar";

interface RecursiveInterface extends Partial<Record<SomeUnion, RecursiveInterface>> {
    a: number;
    b: Array<number>;
}

This works as desired:

const someObject: RecursiveInterface = {
    a: 2, b: [1, 5], foo: {
        a: 0, b: [2, 6], bar: {
            a: 8, b: 7 // error, number is not number[]
        }
    }
}

(at least I think that's what you want, since your example gives an invalid b property at a nested level

  • Related