Home > Mobile >  Comparing strings in typescript before compilation
Comparing strings in typescript before compilation

Time:11-28

I have been developing a package and I found out, that I'd love to show a problem (meaning something like the screenshot just with different message)

1

when there is a wrong value passed to one of the functions.

I have a code that looks something like this:

interface IEdge {
    id: string
}

interface IModule {
    id: string
    data: {
        inputEdges: IEdge[]
    }
}
const sampleModule = {
    id: "sampleModule",
    data: {
        inputEdges: [
            {
                id: "sampleEdge"
            },
            {
                id: "anotherEdge"
            }
        ]
    }
}

class Package {
    module: IModule|null = null
    init(module: IModule) {
        this.module = module
    }
    recieveOnEdge(
        edgeId: string,
        callback: any
    ){
       console.log(edgeId, callback)
    }
}

const packageInstance = new Package()
packageInstance.init(sampleModule)
packageInstance.recieveOnEdge("sampleEdge", "doesNotMatter")

I want to show an problem in editor, when first parameter of .recieveOnEdge is not one of the IDs of inputEdges property on packageInstance.module, but I don't know how to do so, as ID can be whatever string the developer desires.

CodePudding user response:

In order for the compiler to keep track of the string literal types corresponding to valid edge ids, we will need to make Package generic in the union of those types, and all the types and interfaces that hold such ids should be generic as well.

Something like this:

interface IEdge<K extends string = string> {
    id: K
}

interface IModule<K extends string = string> {
    id: string
    data: {
        inputEdges: readonly IEdge<K>[]
    }
}

Here I've added the generic type parameter K to both IEdge and IModule. If you don't specify them they will default to string like your version. Also, when you create sampleModule the compiler will infer just string for those ids unless you give it a hint that it should pay closer attention, such as with a const assertion:

const sampleModule = {
    id: "sampleModule",
    data: {
        inputEdges: [
            {
                id: "sampleEdge"
            },
            {
                id: "anotherEdge"
            }
        ]
    }
} as const;

That as const causes the compiler to infer sampleModule as this type:

/* const sampleModule: {
    readonly id: "sampleModule";
    readonly data: {
        readonly inputEdges: readonly [{
            readonly id: "sampleEdge";
        }, {
            readonly id: "anotherEdge";
        }];
    };
} */
    

So now the compiler definitely knows that "sampleEdge" and "anotherEdge" are valid edge ids. Also note that inputEdges has been inferred as a readonly array type, which is technically wider than a read-write array. That's why I widened the type in IModule<K> to readonly IEdge<K>[]. That probably won't matter unless you need to start modifying that array after the fact (but I hope you don't, since that could change which edge ids are valid).


To make this easy, I'm going to replace your init() method with a constructor argument. That way, the type of your package instance can know about the edge ids from the moment it's created. Otherwise we'd have to try to narrow the type when you call init(), which is hard to do properly. You can technically do it by making init() an assertion method, but it's not fun. Unless someone needs to have a package instance sit around before it's initialized, we should have the initialization done in the constructor, which is more conventional anyway.

Here it is:

class Package<K extends string> {
    constructor(public module: IModule<K>) { }
    recieveOnEdge(
        edgeId: K,
        callback: any
    ) {
        console.log(edgeId, callback)
    }
}

So you can see that receiveOnEdge() only takes an edgeId of type K. Let's test it out:

const packageInstance = new Package(sampleModule);
// const packageInstance: Package<"sampleEdge" | "anotherEdge">    

So the compiler infers that packageInstance is of type Package<"sampleEdge" | "anotherEdge">, leading to the behavior you wanted with receiveOnEdge():

packageInstance.recieveOnEdge("sampleEdge", "doesNotMatter"); // okay
packageInstance.recieveOnEdge("badEdge", "doesNotMatter"); // error

Playground link to code

  • Related