Donald Zhu
Donald Zhu

Reputation: 855

How to use the same parameters with two overloaded functions in TypeScript?

I tried to call another overloaded function within an overloaded function in typescript. Since the type Func2 is identical to the type Func1, it is certain that the arguements passed onto func1 from func2 will be typed correctly. However, typescript seems to be unable to pick that up, and throwing an error.

type Func1 = {
  (a: string, b: string): void
  (a: undefined, b: undefined): void
}
const func1: Func1 = (a, b) => {
  console.log(a, b)
}

type Func2 = {
  (a: string, b: string): void
  (a: undefined, b: undefined): void
}
const func2: Func2 = (a, b) => {
  func1(a, b)
  console.log(a, b)
}

/*
No overload matches this call.
  Overload 1 of 2, '(a: string, b: string): void', gave the following error.
    Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
      Type 'undefined' is not assignable to type 'string'.
  Overload 2 of 2, '(a: undefined, b: undefined): void', gave the following error.
    Argument of type 'string | undefined' is not assignable to parameter of type 'undefined'.
      Type 'string' is not assignable to type 'undefined'.
*/

How can I resolve this?

EDIT: Here's the actual implementation that I was attempting:

type ParseRandomArgs = {
  (a: undefined, b: undefined): [number, number]
  (a: number, b: undefined): [number, number]
  (a: number, b: number): [number, number]
  (a: [number, number], b: undefined): [number, number]
  (a: [number], b: undefined): [number, number]
}

type RandomNumber = {
  (a: undefined, b: undefined): number
  (a: number, b: undefined): number
  (a: number, b: number): number
  (a: [number, number], b: undefined): number
  (a: [number], b: undefined): number
}

const isNullish = (value: any) => value === undefined || value === null

const parseRandomArgs: ParseRandomArgs = (a, b) => {
  if (Array.isArray(a)) {
    if (a.length === 2) return a
    return [0, a[0]]
  }
  else if (isNullish(b)) return [0, isNullish(a) ? 1 : a as number]
  else return [a as number, b as number]
}

const randomFloat: RandomNumber = (a, b) => {
  let [min, max] = parseRandomArgs(a, b) // [min, max]
  return Math.random() * (max - min) + min
}

const randomInt: RandomNumber = (a, b) => {
  let [min, max] = parseRandomArgs(a, b) // [min, max]
  min = Math.ceil(min)
  max = Math.floor(max)
  return Math.floor(Math.random() * (max - min) + min)
}

Upvotes: 1

Views: 1078

Answers (3)

jsejcksn
jsejcksn

Reputation: 33693

You don't need to duplicate your parameters or even use any assertions: you can use a type predicate to let the compiler discriminate nullish types.

And by using labelled tuples for the parameters, you can even get a nicer developer experience via helpful IntelliSense suggestions in your editor:

enter image description here

TS Playground

type Fn<
  Params extends unknown[] = any[],
  Result = any,
> = (...args: Params) => Result;

type CommonParams = [
  [max?: number],
  [min: number, max?: number],
  [minAndMax: [min: number, max: number]],
  [maxOnly: [max: number]],
];

type ParseFn = Fn<CommonParams[number], [number, number]>;
type RandomFn = Fn<CommonParams[number], number>;

function isNullish <T>(value: T): value is Exclude<T, NonNullable<T>> {
  return value === undefined || value === null;
}

const parseRandomArgs: ParseFn = (...args) => {
  const [a, b] = args;
  if (Array.isArray(a)) return a.length === 2 ? a : [0, a[0]];
  if (isNullish(a)) return [0, 1];
  return isNullish(b) ? [0, a] : [a, b];
};

const randomFloat: RandomFn = (...args) => {
  const [min, max] = parseRandomArgs(...args);
  return Math.random() * (max - min) + min;
};

const randomInt: RandomFn = (...args) => {
  let [min, max] = parseRandomArgs(...args);
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min) + min);
};

Upvotes: 2

kingkupps
kingkupps

Reputation: 3494

As @Mike mentioned, making undefined arguments optional makes overloading simpler to work out. This should work:

type RandomGenerator = {
    (start: number, end?: number): number
    (range: [number, number]): number;
    (range: [number]): number;
}

const randomFloat: RandomGenerator = (start, end?) => {
    const [min, max] = parseRandomArgs(start, end as number | undefined);
    return computeRandom(min, max);
}

const randomInt: RandomGenerator = (start, end?) => {
    const [min, max] = parseRandomArgs(start, end as number | undefined);
    return Math.floor(computeRandom(min, max));
}

const computeRandom = (min: number, max: number): number => {
    return Math.random() * (max - min) + min;
}

const parseRandomArgs = (first: number | [number, number] | [number], second: number | undefined): [number, number] => {
    let args: [number, number];
    if (Array.isArray(first) && first.length === 1) {
        args = [0, first[0]];
    } else if (Array.isArray(first) && first.length === 2) {
        args = first;
    } else {
        args = Number.isFinite(second) ? [first, second as number] : [0, first];
    }
    const [min, max] = args;
    return [Math.ceil(min), Math.floor(max)];
}

Upvotes: 1

Mike
Mike

Reputation: 1375

I think you try make too clean code

Considering the second example (with a random number): typescript actually join all possible variants of arguments

const randomInt: RandomNumber = (a, b) => {
   //a: number | [number, number] | [number] | undefined
   //b: number | undefined
}

and your RandomNumber/ParseRandomArgs definitions can be used only for call validation

so the first solution is to extend ParseRandomArgs with the union of all args

type ParseRandomArgs = {
 ...
 (a: [number]| [number, number] | number | undefined, b: number | undefined): [number, number]
}

second, move such union to protected function and convert ParseRandomArgs to a proxy

const _parseRandomArgs = function(a: [number] | [number,number] | number | undefined ,b?: number | undefined): [number,number]{
   ...
}

const parseRandomArgs: ParseRandomArgs = (a, b) => {
  return _parseRandomArgs(a,b);
}

const randomFloat: RandomNumber = (a, b) => {
  let [min, max] = _parseRandomArgs(a, b) // [min, max]
  return Math.random() * (max - min) + min
}

and I suggest you make all undefined arguments - optional

full code in Playground

Upvotes: 1

Related Questions