简体   繁体   中英

Typescript: How to declare a Type which represents a Treeview? Is it possible?

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].yz is the path to access value of z in myObject

 const path = "w1[a2].x[b02].yz"; 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].yz" } }, { idLvlX: "b02", y: { z: "hello world from w1[a1].x[b02].yz" } }, { idLvlX: "b03", y: { z: "hello world from w1[a1].x[b03].yz" } }, { idLvlX: "b04", y: { z: "hello world from w1[a1].x[b04].yz" } } ] }, { idLvlW: "a2", x: [{ idLvlX: "b01", y: { z: "hello world from w1[a2].x[b01].yz" } }, { idLvlX: "b02", y: { z: "hello world from w1[a2].x[b02].yz" } }, { idLvlX: "b03", y: { z: "hello world from w1[a2].x[b03].yz" } }, { idLvlX: "b04", y: { z: "hello world from w1[a2].x[b04].yz" } } ] }, { idLvlW: "a3", x: [{ idLvlX: "b01", y: { z: "hello world from w1[a3].x[b01].yz" } }, { idLvlX: "b02", y: { z: "hello world from w1[a3].x[b02].yz" } }, { idLvlX: "b03", y: { z: "hello world from w1[a3].x[b03].yz" } }, { idLvlX: "b04", y: { z: "hello world from w1[a3].x[b04].yz" } } ] } ] 

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!

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 (eg, 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM