I am trying to define a interface/type to match this data structure.
The leaf properties are type of { dataType: string }, for example "tags", "isRestricted", "city", "postalCode" etc.
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" }
}
}
}
}
}
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" },
},
},
},
},
};
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
.