Make argument of a function required if a generic type is provided

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.

>Solution :

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 [], the empty tuple.

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.

Playground link to code

Leave a Reply