I am trying to create class that I can use for nested collections, hence my functions have the pathArgs and as you can see it is optional, that is because for collections in firebase that are not nested, I do not need any additional paths given.
However, I need this to actually be required if PathIds
is provided.
class FirebaseService<T, PathIds = {}> {
public findOne(id: string, pathArgs?: PathIds): T { return {} as T };
}
type SomeEntity = {
hello: "hello"
}
So if I do:
const firebase = new FirebaseService<SomeEntity>()
// this should be ok, actually pathArgs should NOT be possible to be given to it at all
firebase.findOne("someId");
// this should give me an error saying that the function does not accept a second argument
firebase.findOne("someId", { someRandomProp: "" });
But if I do:
const firebase = new FirebaseService<SomeEntity, { userId: string }>()
// this should now not be okay, and the function should require a second argument
// that is an object and matches { userId: string }
firebase.findOne("someId");
// and this should now be the only valid way to call this function
firebase.findOne("someId", { userId: "" });
The only difference, is the generics given to FirebaseService
Playground link: TS Playground
Edit:
New Playground link: TS Playground
So the solution does work, but now I am getting an error:
Argument of type '[PathArgs<P>]' is not assignable to parameter of type 'PathArgs<P>'
Everywhere where I use the functions, they work as expected now, but this became an internal error.
CodePudding user response:
One approach is to have the second type parameter P
to FirebaseService
control the tuple type of a rest parameter for your methods. If you explicitly specify P
it with some type, then the rest tuple should look like [pathArgs: P]
; otherwise, if it's not specified we can have it default to [the impossible never
type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type
So your class would look like) and then the rest tuple should look like []
.
That is:
type PathArgs<T> = [T] extends [never] ? [] : [pathArgs: T];
declare class FirebaseService<T, P = never> {
public findOne(id: string, ...p: PathArgs<P>): T;
}
(the reason we check [T] extends [never]
instead of T extends never
is because the latter would be a distributive conditional type and as such would behave unexpectedly).
Let's test it out:
const firebase1 = new FirebaseService<SomeEntity>()
firebase1.findOne("someId"); // okay
firebase1.findOne("someId", { someRandomProp: "" }); // error!
// Expected 1 arg, got 2 -> ~~~~~~~~~~~~~~~~~~~~~~
const firebase2 = new FirebaseService<SomeEntity, { userId: string }>()
firebase2.findOne("someId"); // error
// ~~~~~~~~~~~~~~~~~ <-- Expected 2 args, got 1
firebase2.findOne("someId", { userId: "" }); // okay
Looks good! The presence of a second type argument causes a second method argument to be required, and the absence of a second type argument causes a second method argument to be prohibited.