Search code examples
typescriptinterfacetypescript-types

Infinit nested types in typescript


I am trying to define a interface/type to match this data structure.

  1. The leaf properties are type of { dataType: string }, for example "tags", "isRestricted", "city", "postalCode" etc.

  2. The non-leaf properties have other non-leaf properties of leaf properties. For example, "organism", "collection", and "collectionAddress".

Can anyone help define an interface or type for this data structure? Much appreciated for any help!

{
  "sample": {
    "tags": { "dataType": "string[]" },
    "isRestricted": { "dataType": "boolean" },
    "organism": {
      "sex": { "dataType": "string" },
      "collection": {
        "collectionDate": {"dateType": "date"},
        "collectionAddress": {
          "addressLine": { "dataType": "string" },
          "city": { "dataType": "string" },
          "postalCode": { "dataType": "string" }
        }
      }
    }
  }
}


Solution

  • Sounds like you want a recursive record of strings to { dataType: string } or other records. It's tempting to use exactly that here:

    type Leaves<T> = Record<string, T | Leaves<T>>;
    

    but you'll get an error, "Type circularly references itself" (see this question), so we'll have to expand it into a mapped type:

    type Leaves<T> = { [K in string]: T | Leaves<T> };
    

    You could then use it like this:

    const data: Leaves<{ dataType: string }> = {
        sample: {
            tags: { dataType: "string[]" },
            isRestricted: { dataType: "boolean" },
            organism: {
                sex: { dataType: "string" },
                collection: {
                    collectionDate: { dataType: "date" },
                    collectionAddress: {
                        addressLine: { dataType: "string" },
                        city: { dataType: "string" },
                        postalCode: { dataType: "string" },
                    },
                },
            },
        },
    };
    

    Playground


    It is important to note that this solution allows for things such as

    { dataType: { dataType: { dataType: "string" } } }
    

    If you do not want this behavior, you may add another mapped type to map all the keys of T to never:

    type Leaves<T> = { [K in string]: T | Leaves<T> } & { [K in keyof T]?: never };
    

    Then in the previous snippet, you will get an error on the first dataType saying that { ... } is not assignable to type string.

    Playground