1

I have function where the first argument determines the 2nd argument. It acts similar to Foo

type stringF = (type: 'str', value: string) => void
type numberF = (type: 'num', value: number) => void
type booleanF = (type: 'bool', value: boolean) => void
...
... 

declare const Foo: stringF & numberF & booleanF //& etc..

There were 6 functions types in total. It was a pain but manageable. However now there is an extra parameter as the first argument that specifies if it should be an array or not..

so it became:

type stringF = (arr: false, type: 'str', value: string) => void
type numberF = (arr, false, type: 'num', value: number) => void
type booleanF = (arr, false, type: 'bool', value: boolean) => void
...

type stringF = (arr: true, type: 'str', value: string[]) => void
type numberF = (arr, true, type: 'num', value: number[]) => void
type booleanF = (arr, true, type: 'bool', value: boolean[]) => void
...

Now there are 12 function types. And it doesn't seem worth the hassle to properly type the function

Is there any easier way to make conditional function signitures?

lonewarrior556
  • 3,917
  • 2
  • 26
  • 55

2 Answers2

1

You can use conditional types to create a single signature for all the possibilities:

type StringToType = { 
  str: string,
  num: number,
  bool: boolean
}
type MakeArrayIfTrue<TCondition, T> = TCondition extends true ? T[] :T;

type fn =  <MakeArray extends boolean, TypeKey extends keyof StringToType>(arr:  MakeArray, type: TypeKey, value: MakeArrayIfTrue<MakeArray, StringToType[TypeKey]>) => void

declare let fn : fn;
fn(true, "str", [""]);
fn(true, "bool", [""]); // error
fn(false, "str", [""]); // error

This behaves a bit different then multiple overloads if the parameter is a union. So for example this is valid:

declare let b: boolean;
declare let strOrBool: "str" | "bool";
// Last parameter ca be boolean | string | boolean[] | string[]
fn(b, strOrBool, "") //ok
fn(b, strOrBool, [""]) //ok
fn(b, strOrBool, true) //ok
fn(b, strOrBool, 1) //this is still an error 

If you want to restrict this behavior we can get a bit more creative and first create a union of all possible signatures and then use UnionToIntersection to get back to a type that behaves like a function with overloads:

type StringToType = { 
  str: string,
  num: number,
  bool: boolean
}
type MakeArrayIfTrue<TCondition, T> = TCondition extends true ? T[] :T;

type UnionToIntersection<U> =  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type FnHelper<MakeArray extends boolean = boolean, TypeKey extends keyof StringToType = keyof StringToType> = 
    MakeArray extends any ? TypeKey extends any ? (arr:  MakeArray, type: TypeKey, value: MakeArrayIfTrue<MakeArray, StringToType[TypeKey]>) => void: never: never;
type fn = UnionToIntersection<FnHelper>;

declare let fn : fn;
fn(true, "str", [""]);
fn(true, "bool", [""]); // error
fn(false, "str", [""]); // error

declare let b: boolean;
declare let strOrBool: "str" | "bool";
fn(b, strOrBool, "") //error
fn(b, strOrBool, [""]) //error
fn(b, strOrBool, true) //error
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
0

You should use Generic in this case. So your function signature may look like this:

function myFunc<T>(value: T|T[]): void

It could be array or not array, you just need to check it inside your function. And the type T represent all the different value types.

Xinan
  • 3,002
  • 1
  • 16
  • 18