1

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.

6
  • Could you provide a self-contained minimal reproducible example that actually demonstrates your issue when pasted into a standalone IDE? Right now if I do so I get several errors unrelated to your question. Commented Nov 27, 2022 at 21:09
  • You'll need generic class, but I won't tell how until you minimal reproducible example Commented Nov 27, 2022 at 21:13
  • 1
    @jcalz Sorry, should be working fine now. Commented Nov 27, 2022 at 21:26
  • You will have a much better time if you use constructor arguments than use a separate init() step. Why are you using init here? What's the use of having a Package() that can sit around in an uninitialized state? Commented Nov 27, 2022 at 21:27
  • 1
    @jcalz I'll most likely end up using constructor, otherwise up to you. Commented Nov 27, 2022 at 21:42

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.