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)
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