Home > Enterprise >  Type guard based on function arity
Type guard based on function arity

Time:11-26

Let's say I have a function with multiple optional parameters.

Why is there no type guard based on the function arity through the arguments keyword and how to solve this without changing the implementation and without ugly casts?

function test(arg0?: string, arg1?: string, arg2?: string) {

    if (arguments.length === 1) {
        // I expect arg0 to be of type string instead of string | undefined
    }

    if (arguments.length === 2) {
        // I expect arg0 and arg1 to be of type string instead of string | undefined
    }

    if (arguments.length === 3) {
        // I expect arg0, arg1 and arg2 to be of type string instead of string | undefined
    }
}

CodePudding user response:

Please see arguments type:

interface IArguments {
    [index: number]: any;
    length: number;
    callee: Function;
}

interface IArguments {
    /** Iterator */
    [Symbol.iterator](): IterableIterator<any>;
}

As you might have noticed, keyword arguments knows nothing about arity of arguments type. From typescript perspective it is not even bound to function context. It just an object with a bunch of unrelated to function arguments properties.

Moreover, length property does not act like typeguard. You are unable to infer for example [string, string] type just by checking if length === 2. Please see this answer. You will find there appropriate links to github issues and this hasLengthAtLeast helper.

CodePudding user response:

The currently accepted answer is quite good, making use of a type predicate to narrow the type.

I would suggest an alternate way, making use of rest parameters, like so:

function test(...args: string[]) {
  if (args.length === 1) {
    console.log('arg0:', typeof args[0]);
  }

  if (args.length ==- 2) {
    console.log('arg0:', typeof args[0], 'arg1:', typeof args[1]);
  }

  if (args.length === 3) {
    console.log('arg0:', typeof args[0], 'arg1:', typeof args[1], 'arg2:', typeof args[2]);
  }
}

When used in with various arguments, we get the following output:

Input Compiles? Output
test('one'); yes arg0: string
test('one', 'two', 'three'); yes arg0: string arg1: string arg2: string
test(1); no error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
test(undefined); no error TS2345: Argument of type 'undefined' is not assignable to parameter of type 'string'.

One (potentially major) downside of this approach is that the length of the args array is not limited. Depending on your use case it could be sufficient to ignore excess arguments, or you could perform a runtime check like so:

if (args.length > 3) {
  throw new Error(`Expected 3 arguments, but got ${args.length}`);
}

Food for thought.

  • Related