Search code examples
typescriptinterfacearrow-functionsgeneric-type-argumentunion-types

Determine the interface used for a union interface


Suppose I have the following:

interface StringOp {
    (a: string, b: string): string;
}
interface NumberOp {
    (a: number, b: number): number;
}
function doThing(callback: StringOp | NumberOp) {
    if (callback is StringOp) {
        callback("a", "b");
    } else {
        callback(1, 2);
    }
}

How can I actually express callback is StringOp as a type-check?


I tried to simplify the above as an MWE, but below closer to my actual use-case:

interface TickFunction<T> {
  (val: T): void;
}
interface IndexedTickFunction<T> {
  (val: T, index: number): void;
}
function forEachTick<T>(callback: TickFunction<T> | IndexedTickFunction<T>) {
  ...
}

If possible, I'd like to keep calling forEachTick using arrow-notation literals:

  • forEachTick<SomeType>((v) => { ... }) or
  • forEachTick<SomeType>((v,i) => { ... }).

Solution

  • In TS interfaces exist only during development process so there is no way to check interface type in runtime and do smth. So you have to somehow include an "indicator" to your interface in order to set it with some value when you create your callback methods. This indicator can be used to check callback type in runtime. TS also provides User-Defined Type Guards so the final solution will look like:

    interface StringOp {
        opType: 'StringOp',
        (a: string, b: string): string;
    }
    interface NumberOp {
        opType: 'NumberOp',
        (a: number, b: number): number;
    }
    
    function isStringOp(op: StringOp | NumberOp): op is StringOp {
        return op.opType === 'StringOp';
    }
    
    function doThing(callback: StringOp | NumberOp) {
        if (isStringOp(callback)) {
            callback("a", "b");
        } else {
            callback(1, 2);
        }
    }
    

    I've added another example according to the updated question:

    interface TickFunction<T> {
      (val: T): void;
    }
    interface IndexedTickFunction<T> {
      (val: T, index: number): void;
    }
    
    function isTickFn<T>(fn: TickFunction<T> | IndexedTickFunction<T>): fn is TickFunction<T> {
        // in your example the indicator might be the function length
        // because it indicates the number of arguments expected by the function
        return fn.length === 1;
    }
    
    // I guess you also have to pass arguments to this function in order to pass them to your callback methods
    function forEachTick<T>(callback: TickFunction<T> | IndexedTickFunction<T>, value: T, index?: number) {
      if (isTickFn(callback)) {
        callback(value);
      } else {
        callback(value, index);
      }
    }
    
    for (let i=0; i<10; i++) {
      forEachTick<string>((v: string) => console.log(v), 'some text');
    }
    for (let i=0; i<10; i++) {
      forEachTick<boolean>((v: boolean, index: number) => console.log(v, index), true, i);
    }