I'd like to define a type that is recursive on itself like this, essentially:
interface CSSProperties {
marginLeft?: string | number
[key: string]?: CSSProperties
}
Unfortunately, the typescript docs say:
While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type. This is because a string index declares that obj.property is also available as obj[“property”]. In the following example, name’s type does not match the string index’s type, and the type-checker gives an error:
Which seems to say this is impossible to express in typescript, which seems a severe limitation. Flow does what I consider the right thing here and assumes that marginLeft
does not fall into the index specification.
Is this possible at all in TypeScript? Alternatively, is there a way to specify that a string is any string but a set of strings? That way, I could do something roughly like:
interface NestedCSSProperties: CSSProperties {
[P not in keyof CSSProperties]?: CSSProperties
}
The problem here is not really with recursion (which is allowed) but with the conflicting signatures as you pointed out.
I'm not sure the opposite of the current behavior would be correct. It seems like a subjective decision to me with cases that can be made for both ways. Accepting the example as-is means you're merging the definitions implicitly; you may look at one of the lines and assume an effect of the interface while another line changes the outcome. It does feel more natural to write but I'm not sure it's as safe as you don't normally expect fall throughs on type definitions.
Regardless. TypeScript does allow a similar behavior as to what you're expecting, but you have to be explicit in that string keys can also be of type string
or number
. This will work:
interface CSSProperties {
marginLeft?: string | number,
[key: string]: CSSProperties|string|number,
}
For example, with the above interface, this is valid:
let a: CSSProperties = {
marginLeft: 10,
name: {
marginLeft: 20,
}
};
This is not:
let a: CSSProperties = {
marginLeft: 10,
something: false, // Type 'boolean' is not assignable to type 'string | number | CSSProperties'.
something: new RegExp(/a/g), // Type 'RegExp' is not assignable to type 'CSSProperties'.
name: {
marginLeft: 20,
},
car: ["blue"], // Type 'string[]' is not assignable to type 'CSSProperties'.
};
It'll know the named members correctly:
let name1: string | number = a.marginLeft; // OK, return type is string | number
a.marginLeft = false; // Blocked, Type 'false' is not assignable to type 'string | number'.
a["whatever"] = false; // Blocked, Type 'false' is not assignable to type 'string | number | CSSProperties'.
a["marginLeft"] = false; // Blocked, Type 'false' is not assignable to type 'string | number'.
The problem here, however, is that you need to cast other dynamic members when reading - it won't know it's a CSSProperties
.
This won't get blocked:
a["whatever"] = 100;
And it will complain about this:
let name3: CSSProperties = a["name"]; // Type is CSSProperties | string | number
But this will work if you typecast explicitly:
let name3: CSSProperties = a["name"] as CSSProperties;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With