I have an interface ActionTupel
export interface ActionTupel<T extends ID> {
id: T;
// based on a given ID it returns a list of rest-params
// ( ID.test [string,number], ID.print [])
params: ExecutionParams<T>;
}
export type ExecutionParams<T extends ID> =
T extends ID.test ? [string,number] :
T extends ID.print ? [] : [];
and a function to use it
public executeActions<C extends ID>(...actions: ActionTupel<C>[]): Result<Last<C>>
Now I have a question related to these functions:
Question
executeActions(
{
id: ID.test,
params: ['test', 1],
},
{
id: ID.print,
params: ["12", 1],
})
if I try to execute this function with two IDs, it won't throw an error for ID.print
because the array definition is now <ID.print | ID.test>
. How can I achieve to throw an error here? params for ID.print
should be an empty array like []
Added minimal typescript code here without any external libs:
class TestAction implements Action<ID.test, string> {
readonly type = '[Action] Test';
execute(foo: string, bar: number): string {
return 'hello test service';
}
}
class PrintAction implements Action<ID.print, Date> {
readonly type = '[Action] Test';
execute(): Date {
return new Date();
}
}
interface ActionTupel<T extends ID> {
id: T;
params: ExecutionParams<T>;
}
type ExecuteResult<T extends ID> = ReturnType<ActionClass<T>['execute']>;
/**
* don't know which aproach is best to handle selecting last of array
*/
type LengthOfTuple<T extends any[]> = T extends { length: infer L }
? L
: never;
type DropFirstInTuple<T extends any[]> = ((...args: T) => any) extends (
arg: any,
...rest: infer U
) => any
? U
: T;
type LastInTuple<T extends any[]> = T[LengthOfTuple<
DropFirstInTuple<T>
>];
type Last<T extends []> = T extends [...unknown[], infer R] ? R : never;
/**
* end
*/
enum ID {
print = 'print',
test = 'test',
}
type ActionClass<T extends ID> = T extends ID.print
? PrintAction
: T extends ID.test
? TestAction
: never;
type ExecutionParams<T extends ID> = Parameters<
ActionClass<T>['execute']
>;
type FunctionType<T extends ID> = ReturnType<ActionClass<T>['execute']>;
interface ActionSet<T extends ID> {
loadService: () => ActionClass<T>
}
const ACTIONS = new Map<ID, ActionSet<ID>>()
.set(ID.print, {
loadService: () => new PrintAction(),
})
.set(ID.test, {
loadService: () => new TestAction(),
});
interface Action<C extends ID, T> {
readonly type: string;
// describes the chainable function of each action
execute: (...params: ExecutionParams<C>) => T;
}
const getAction = <T extends ID>(id: T): ActionSet<T> => {
if (!ACTIONS.has(id)) {
return null;
}
return ACTIONS.get(id);
};
class ActionService {
/**
* whether the id is known by the service or not
* @param {ID} id unique id of the action
* @returns boolean
*/
public hasValidAction<T extends ID>(id: T): boolean {
return getAction(id) ? true : false;
}
public async executeActions<C extends ID>(
...actions: ActionTupel<C>[]
): Promise<ExecuteResult<LastInTuple<C[]>>> {
const items = actions.map((a) => this.executeAction(a.id, ...a.params));
const r = await Promise.all(items);
return r[1];
}
/**
* executes a single action by checking for necessary services
* @param {ID} id unique id of the action
* @returns Observable<T>
*/
public async executeAction<T extends ID>(
id: T,
...args: ExecutionParams<T>
): Promise<ExecuteResult<T>> {
const s = await this.getService(id);
console.log(s.type);
const r = s.execute.apply(this, args) as FunctionType<T>;
return r;
}
/**
* retreives service based on passed action id
* @param {ID} id unique action id
* @returns Promise<ActionClass<T>>
*/
private async getService<T extends ID>(id: T): Promise<ActionClass<T>> {
const actionSet = getAction(id);
if (!actionSet) {
return null;
}
const s = await actionSet.loadService();
return s;
}
}
const service = new ActionService();
// fine
const printR = service.executeAction(ID.print);
// fine
const testR = service.executeAction(ID.test, 'hallo', 1);
// incomplete
const finalR = service.executeActions(
{
id: ID.test,
params: ['test', 1],
},
{
id: ID.print,
params: ['test', 1], // needs an assertion to be empty
}
);
CodePudding user response:
I'm assuming:
enum ID {
test,
print
}
1
I think what's happening is that TS is allowing C
to be a different value that test
or print
.
Being more specific works for me:
executeActions(...actions: (ActionTupel<ID.test> | ActionTupel<ID.print>)[])
or alternatively:
type StricterActionTuple = ActionTupel<ID.test> | ActionTupel<ID.print>;
executeActions(...actions: StricterActionTuple<ID.test>[]);
Answer to old second question:
2
type Last<Ar extends []> = Ar extends [ ...unknown[], infer U] ? U : never;
Putting it together:
type Last<Ar extends []> = Ar extends [ ...unknown[], infer U] ? U : never;
type StricterActionTuple = ActionTupel<ID.test> | ActionTupel<ID.print>;
declare function executeActions<C extends StricterActionTuple[]>(...actions: C): Last<C>;
CodePudding user response:
As you saw, the problem with this:
declare function executeActions<C extends ID>(...actions: ActionTuple<C>[]): any;
is that C
is inferred as a union type of all the values passed in for id
, and therefore params
is also allowed to be a union of the corresponding parameters. And so it will accept things you don't want to be accepted:
executeActions(
{ id: ID.test, params: ['test', 1] },
{ id: ID.print, params: ["12", 1] } // no error
); // C inferred as ID
A potential fix is to make executeActions()
generic in the tuple of values passed in for id
. That is, if the first argument to executeActions()
has an id
of ID.test
and the second has an id
of ID.print
, then we want the type argument to be [ID.test, ID.print]
.
It could look like this:
declare function executeActions<C extends ID[]>(
...actions: { [I in keyof C]: ActionTuple<C[I]> }
): any;
Here, the actions
rest parameter is of a mapped tuple type over C
, the tuple of id
properties. Each element of C
at index I
(also known as C[I]
) is mapped an element of type ActionTuple<C[I]>
. So if C
is [ID.test, ID.print]
, then actions
is of type [ActionTuple<ID.test>, ActionTuple<ID.print>]
.
Let's test it out:
executeActions( // C inferred as [ID.test, ID.print]
{ id: ID.test, params: ['test', 1] },
{ id: ID.print, params: ["12", 1] } // error!
// -----------> ~~~~~~
// Type '[string, number]' is not assignable to type '[]'
);
Looks good. The type checker is able to infer C
as [ID.test, ID.print]
from the arguments passed in for executeActions()
, and then the second argument is seen to be in error because params
for ActionTuple<ID.print>
must be of tpe []
, not [string, number]
.