In Typescript, all of these 3 types are similar, in that they are only made of primitive types, maps and arrays:
type Color1 = { [component: string]: number }
type Color2 = {
green: number
red: number
blue: number
}
interface Color3 {
green: number
red: number
blue: number
}
How does one design a type that will match all of the definitions above?
The obvious definition doesn't work for Color3
:
type JsonPrimitive = string | number | boolean | null
type JsonifiableObject = { [Key in string]?: Jsonifiable }
type JsonifiableArray = readonly Jsonifiable[]
type Jsonifiable = JsonPrimitive | JsonifiableObject | JsonifiableArray
const color1 = {green: 1, red: 2, blue: 3} as Color1 as Jsonifiable
const color2 = {green: 1, red: 2, blue: 3} as Color2 as Jsonifiable
const color3 = {green: 1, red: 2, blue: 3} as Color3 as Jsonifiable
yielding:
Conversion of type 'Color3' to type 'Jsonifiable' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'Color3' is not comparable to type 'JsonifiableObject'.
Index signature for type 'string' is missing in type 'Color3'.
There is no specific type that works the way you want. If you have an object type with a string
index signature like JsonifiableObject
(which should be {[k: string]: Jsonifiable}
or maybe {[k: string]: Jsonifiable | undefined}
, depending on whether you really want to use the ?
modifier), then it will never be compatible with an interface that lacks an index signature. That is, interfaces are not given implicit index signatures. This is documented in microsoft/TypeScript#15300 and is considered by design.
You either need to give up or work around it.
If you need to match an interface type to something like this, you'll need to change the index signature to a generic key instead, meaning that your Jsonifiable
type becomes a JsonifiableValidate<T>
type such that T extends JsonifiableValidate<T>
if and only if T
is valid. It becomes like a generic constraint:
type JsonifiableValidate<T> = T extends JsonPrimitive ? T :
T extends readonly (infer A)[] ? readonly (JsonifiableValidate<A>)[] :
T extends Function ? never :
T extends object ? { [K in keyof T]: JsonifiableValidate<T[K]> } : never;
And since generic types don't have type argument inference (it's a missing feature documented at microsoft/TypeScript#32794) you'd need to make a generic helper identity function like this:
const jsonifiable = <T,>(t: JsonifiableValidate<T>) => t;
And then instead of const color: Jsonifiable = {⋯}
you write const color = jsonifiable({⋯})
:
const color = jsonifiable({ green: 1, red: 2, blue: 3 } as Color3); // okay