typescriptinterfaceoption-typerecursive-datastructuresmapped-types

How to properly declare a recursive interface with optional properties of a mapped type


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?


Solution

  • 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 🤷‍♂️)

    Playground link to code