Reputation: 275
Typescript is a TYPED superset of JavaScript that compiles into JavaScript, fine! it helps us to reduce some typos etc etc ok! I want to create an interface that would be used as an argument in a method. This interface has to represent a treeview that would be used to parse an object.
example: w1[a2].x[b02].y.z
is the path to access value of z
in myObject
const path = "w1[a2].x[b02].y.z";
const treeOfIdentifiers = {
"w1": {
key: "idLvlW",
"x": {
key: "idLvlX"
}
}
}
const myObject = {
w0: "Hello Root",
w1: [{
idLvlW: "a1",
x: [{
idLvlX: "b01",
y: {
z: "hello world from w1[a1].x[b01].y.z"
}
},
{
idLvlX: "b02",
y: {
z: "hello world from w1[a1].x[b02].y.z"
}
},
{
idLvlX: "b03",
y: {
z: "hello world from w1[a1].x[b03].y.z"
}
},
{
idLvlX: "b04",
y: {
z: "hello world from w1[a1].x[b04].y.z"
}
}
]
},
{
idLvlW: "a2",
x: [{
idLvlX: "b01",
y: {
z: "hello world from w1[a2].x[b01].y.z"
}
},
{
idLvlX: "b02",
y: {
z: "hello world from w1[a2].x[b02].y.z"
}
},
{
idLvlX: "b03",
y: {
z: "hello world from w1[a2].x[b03].y.z"
}
},
{
idLvlX: "b04",
y: {
z: "hello world from w1[a2].x[b04].y.z"
}
}
]
},
{
idLvlW: "a3",
x: [{
idLvlX: "b01",
y: {
z: "hello world from w1[a3].x[b01].y.z"
}
},
{
idLvlX: "b02",
y: {
z: "hello world from w1[a3].x[b02].y.z"
}
},
{
idLvlX: "b03",
y: {
z: "hello world from w1[a3].x[b03].y.z"
}
},
{
idLvlX: "b04",
y: {
z: "hello world from w1[a3].x[b04].y.z"
}
}
]
}
]
What would be the type|interface of treeOfIdentifiers
(if not any
!) if I code using TypeScript? The thing is to ensure that each node of treeOfIdentifiers, the property key
would be provided and we don't know the structure for the treeOfIdentifiers
as we don't know the structure of the object to parse!
Upvotes: 0
Views: 263
Reputation: 329248
This is a fairly difficult thing to express in TypeScript. For example, adding a string-valued property named key
to a dictionary of non-string
values is not straightforward to strongly type... and possibly implies that you might want to make the type simpler (e.g., each node has a key
property and a dictionary
property).
I'm not even thinking of making sure that treeOfIdentifiers
is valid when using it to traverse myObject
, since you didn't ask about that and this is already very complicated.
But let's see how far we can get, using mapped and conditional types:
type NonRootOfTreeOfIdentifiers<T> = { [K in 'key' | keyof T]:
K extends 'key' ? string :
K extends keyof T ? NonRootOfTreeOfIdentifiers<T[K]> : never
};
type TreeOfIdentifiers<T> = { [K in keyof T]: NonRootOfTreeOfIdentifiers<T[K]> };
const asTreeOfIdentifiers = <T extends TreeOfIdentifiers<T>>(t: T): T => t;
The trickiest part of that is in NonRootOfTreeOfIdentifiers<T>
. It takes a type T
representing a non-root node in a valid treeOfIdentifiers
. It adds a key
property to T
, and then maps these properties of T
into a new type: key
is mapped as a string
property, and every other property is mapped to a NonRootOfTreeOfIdentifiers<>
version of itself. So NonRootOfTreeOfIdentifiers<T>
walks down through the levels of T
and converts it to another type. If T
is a valid non-root node, then NonRootOfTreeOfIdentifiers<T>
will be a valid non-root node.
The TreeOfIdentifiers
type function represents the root node, which doesn't require a key
property, but otherwise traverses down into NonRootOfTreeOfIdentifiers
.
Finally, the asTreeOfIdentifiers
function takes an argument of type T
which is required to be compatible with TreeOfIdentifiers<T>
, and returns the argument. Meaning, it will only accept objects conforming to the rules. So it validates its argument but doesn't change it.
Let's see if it works:
const treeOfIdentifiers = asTreeOfIdentifiers({
"w1": {
key: "idLvlW",
"x": {
key: "idLvlX"
}
}
});
That compiles, and treeOfIdentifiers
is inferred to be of type
{
"w1": {
key: string;
"x": {
key: string;
};
};
}
If you mess up in some way, you will get errors:
const missingKey = asTreeOfIdentifiers({
"w1": {
key: "idLvlW",
"x": {
kee: "idLvlX" // oops
}
}
});
// error: Property 'key' is missing in type '{ kee: string; }'
or
const extraProperty = asTreeOfIdentifiers({
"w1": {
key: "idLvlW",
x: {
key: "idLvlX"
}
y: 123 // oops
}
});
// error: Types of property 'y' are incompatible.
// Type 'number' is not assignable to type 'NonRootOfTreeOfIdentifiers<number>'.
So that all works as far as it goes. I'm not sure it's worth it to you, though. It gets even hairier if you try to capture the literal string value types like "idLvlX"
(instead of just string
). And, as I said, I haven't even thought about how you'd validate myObject
against treeOfIdentifiers
in the type system.
Anyway, good luck. Hope that helped.
Upvotes: 2