Closed wSedlacek closed 3 years ago
Hey, thanks for suggestion. I will look into it and have talk with @GrandSchtroumpf who is the owner of this lib.
Here is another example that supports an interfaces can just specify params with no return value (which from my experience tends to be most used use case)
Note: I have adjust the naming a bit to be more reflective of what each part actually does ie. FunctionFactoryService
type Serializable = string | number | boolean | null | void | SerializableObject | Serializable[];
interface SerializableObject {
[key: string]: Serializable;
}
type Params<T extends ((params: Serializable) => unknown) | Serializable | any> = T extends (
params: infer I
) => any
? I extends Serializable
? I
: void
: T;
type Returns<T extends ((params: unknown) => Serializable | any) | any> = T extends (
params: any
) => infer I
? I extends Serializable
? I
: void
: void;
@Injectable({ providedIn: 'root' })
export class FunctionFactoryService<
Functions extends Record<string, ((arg: Serializable) => Serializable) | Serializable | any>
> {
constructor(private readonly functions: AngularFireFunctions) {}
/**
* @description Factory to create a type safe call of a cloud function
* @param name of the cloud function
* @param params of the cloud function
*/
public create<FunctionName extends keyof Functions>(
name: FunctionName
): (params: Params<Functions[FunctionName]>) => Promise<Returns<Functions[FunctionName]>> {
return async (params) => {
const callableFunction = this.functions.httpsCallable(name.toString());
return callableFunction(params).toPromise();
};
}
}
export interface VoteParams {
snackID: string;
}
export interface VoteFunctions {
vote: VoteParams;
foo(): number;
bar(params: string): number;
baz(params: string): void;
}
@Injectable({ providedIn: 'root' })
@CollectionConfig({ path: 'votes' })
export class VotesService extends CollectionService<VotesState> {
constructor(
protected readonly store: VotesStore,
private readonly functionFactory: FunctionFactoryService<VoteFunctions>
) {
super(store);
}
// (property) VotesService.voteFor: (params: VoteParams) => Promise<void>
public readonly voteFor = this.functionFactory.create('vote');
// (property) VotesService.foo: (params: void) => Promise<number>
public readonly foo = this.functionFactory.create('foo');
// (property) VotesService.bar: (params: string) => Promise<number>
public readonly bar = this.functionFactory.create('bar');
// (property) VotesService.baz: (params: string) => Promise<void>
public readonly baz = this.functionFactory.create('baz');
}
Hmm I wouldn't create a full service for this, especially not for typing. I would just improve the callFunction bind :
export async function callFunction<
C extends Record<string, (...args: any) => any>,
N extends Extract<keyof C, string>
>(
functions: AngularFireFunctions,
name: N,
params?: Parameters<C[N]>
): Promise<ReturnType<C[N]>> {
const callFunction = functions.httpsCallable(name);
return callFunction(params).toPromise();
}
The code above is not 100% perfect (params become array & you need to specify the name in the angle brackets). But it's a good base to find the right type interface.
After having time to reflect on this I think this might be an upstream issue and that a OR to make @angular/fire/funtions type safe may be better.
In the latest update the
callFunction()
function was added to the public API. This function as it stands is not Type Safe. I feel like we could go one step further and offer aFunctionsService
that provides a Type Safe solution (particularly useful in mono repos)Below is an example of what a service like this would look like.
It would be consumed like so.
Then the cloud function might be setup like so.