Home > Back-end >  Checking generic type argument in array of mixed generics
Checking generic type argument in array of mixed generics

Time:10-14

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].

Playground link to code

  • Related