0

what am i trying to do is, i have a function and I'm passing a bunch of boolean flags to it, each of these flags requires different arguments to be passed to same function.

So here is a simple version:


enum FLAGS {
  A = 'a',
  B = 'b',
  C = 'c'
}

interface ARelatedArgs {
  aBool: boolean
  aString: string
}

interface BRelatedArgs {
  (bEvt: any): void
  bBool: boolean
}

interface CRelatedArgs {
  (cEvt: any): string
  cString: string
}


interface MyFunctionArgs {
  flags: Partial<Record<FLAGS, boolean>>
  // other props based on flag(s) // i can supply one or more flags here!
}

function myFunction(args: MyFunctionArgs) {
  // do something 
}

now i want to have type inference based on these call types:


// 1st Call to my function
myFunction({
  flags: { [FLAGS.A]: true }
  // all ARelatedArgs
})


// 2nd Call to my function
myFunction({
  flags: { [FLAGS.A]: true, [FLAGS.B]: true }
  // all ARelatedArgs
  // + all BRelatedArgs
})

// last Call to my function
myFunction({
  flags: { [FLAGS.A]: true, [FLAGS.B]: true, [FLAGS.C]: true }
  // all ARelatedArgs
  // + all BRelatedArgs
  // + all CRelatedArgs 
})

basically what i want is to check argument types/infer them based on flags that is been passed to it. The interesting thing is that, I dont want to make checks only based on one flag, I could have passed all my 3 flags (A, B, C) and IntelliSense may help me with that? is it possible to have this behavior in typescript?

I know I'm demanding runtime checking from typescript but any idea how can i implement such a thing?

basically i want MyFunctionArgs to be something like this:

interface MyFunctionArgs {
  flags: Partial<Record<FLAGS, boolean>>
  ...(FLAGS.A in flags && ARelatedArgs),
  ...(FLAGS.B in flags && BRelatedArgs),
  ...(FLAGS.C in flags && CRelatedArgs),
}

Thanks in advance

amdev
  • 6,703
  • 6
  • 42
  • 64
  • should args look like this? `{flags: { [FLAGS.A]: true }, a: {}}` – wonderflame Jul 26 '23 at 12:40
  • yes, you can consider args as the complete arguments list based on flags that is been passed to the function. So it can have all Arelated arguments, or A and B related arguments or A, B and C related arguments all at same time if theire respective flags is already passed – amdev Jul 26 '23 at 12:42
  • is it okay for you to have them flags and related fields in two separate arguments? – wonderflame Jul 26 '23 at 12:48
  • 1
    Like [this](https://tsplay.dev/wQJp7N) – wonderflame Jul 26 '23 at 12:51
  • it is already separated, so it should not be a concern i think – amdev Jul 26 '23 at 12:51
  • If the previous approach works, then I will write an answer explaining; If not, what am I missing? – wonderflame Jul 26 '23 at 12:53
  • that is cool solution, but is there a way to send those arguments all in the same level? not in nested objects ? that would be the ideal solution that i'm looking for – amdev Jul 26 '23 at 12:53
  • 1
    sure, I separated just because there might be collisions – wonderflame Jul 26 '23 at 12:53
  • 1
    check [this one](https://tsplay.dev/NDgMxw) – wonderflame Jul 26 '23 at 12:55
  • great, just one more favour to ask, since this solution is going to be used in react context, I'm looking for passing the relatedArgs next to the flag as props, a use case like this the the best: https://www.typescriptlang.org/play?ts=5.0.4#code/FDD0oAgGwQwZwC4QMIylCCD2EC2BPCAMwFcA7AYwQEsszgCAxcq2sgCmAggG8vvisAOZwAXLwgBtRgBkAggHEAygDo5AXXEIATiQCmAGimzFqgEKbMuw8fnKVySzv0QAvgf7cYZrFihbrDwEIGCUdajIhcQByaKCBACMfP3EiNDhDTwgKMO0IqIhY+PdgAEoAbmAgA – amdev Jul 26 '23 at 12:58
  • 1
    [playground](https://tsplay.dev/m07MaN) – wonderflame Jul 26 '23 at 13:00
  • Awesome, if you copy/paste it here as an answer, I would accept and upvote! a big thanks to you my friend – amdev Jul 26 '23 at 13:02
  • sure, will do in a bit – wonderflame Jul 26 '23 at 13:02

1 Answers1

1

We will need a some key-value map that would link the flags enum with the respective types:

// type TypeMap = {
//     a: ARelatedArgs;
//     b: BRelatedArgs;
//     c: CRelatedArgs;
// }
type TypeMap = Prettify<
  {
    [K in FLAGS.A]: ARelatedArgs;
  } & { [K in FLAGS.B]: BRelatedArgs } & { [K in FLAGS.C]: CRelatedArgs }
>;

Prettify is a utility type that makes the end types more readable, by removing & and some other things.

Next, we will create a utility that by using mapped types will map through the passed flags and by using key remapping it will only keep arguments that we need:

type FlagsArgs<T extends MyFunctionArgs['flags']> = {
  [K in keyof T & keyof TypeMap as T[K] extends true ? K : never]: TypeMap[K];
};

// type Result = {
//     a: ARelatedArgs;
//     b: BRelatedArgs;
// }
type Result = FlagsArgs<{ [FLAGS.A]: true; [FLAGS.B]: true }>;

To turn into the desired shape we will need the following types: ValueOf - returns all values of an object under every key as a union:

type ValueOf<T> = T[keyof T];

Testing:

type FlagsArgs<T extends MyFunctionArgs['flags']> = ValueOf<{
  [K in keyof T & keyof TypeMap as T[K] extends true ? K : never]: TypeMap[K];
}>;

// type Result = ARelatedArgs | BRelatedArgs
type Result = FlagsArgs<{ [FLAGS.A]: true; [FLAGS.B]: true }>;

Now, we need to turn the union into an intersection, by using UnionToIntersection described in here:

type FlagsArgs<T extends MyFunctionArgs['flags']> = UnionToIntersection<
  ValueOf<{
    [K in keyof T & keyof TypeMap as T[K] extends true ? K : never]: TypeMap[K];
  }>
>;

// type Result = ARelatedArgs & BRelatedArgs
type Result = FlagsArgs<{ [FLAGS.A]: true; [FLAGS.B]: true }>;

We can also wrap the FlagsArgs with Prettify to make the result type more literal:

type FlagsArgs<T extends MyFunctionArgs['flags']> = Prettify<
  UnionToIntersection<
    ValueOf<{
      [K in keyof T & keyof TypeMap as T[K] extends true
        ? K
        : never]: TypeMap[K];
    }>
  >
>;

// type Result = {
//     aBool: boolean;
//     aString: string;
//     bBool: boolean;
// }
type Result = FlagsArgs<{ [FLAGS.A]: true; [FLAGS.B]: true }>;

Looks great!

Usage:

function myFunction<T extends MyFunctionArgs>(args: T & FlagsArgs<T['flags']>) {
  // do something
}

Final testing:

// 1st Call to my function
myFunction({
  flags: { [FLAGS.A]: true },
  aBool: true,
  aString: 'str',
});

// 2nd Call to my function
myFunction({
  flags: { [FLAGS.A]: true, [FLAGS.B]: true },
  aBool: true,
  aString: '',
  bBool: false,
});

// last Call to my function
myFunction({
  flags: { [FLAGS.A]: true, [FLAGS.B]: true, [FLAGS.C]: true },
  aBool: true,
  aString: '',
  bBool: false,
  cString: '',
});

playground

wonderflame
  • 6,759
  • 1
  • 1
  • 17
  • Hi Again, In case if you are online/avaliable, how we can pluck (for example) ARelatedArgs from `args` and use it within `myFunction()`? – amdev Aug 21 '23 at 13:42
  • I tried to get it like `function myFunction({ ARGS_FROMMyFunctionArgs, aBool, aString, bBool, cString }: T & FlagsArgs) {` but there was error below `aBool`, `aString`, `bBool` and `cString` – amdev Aug 21 '23 at 13:43
  • basically you could not extract arguments based on flag and use it within function – amdev Aug 21 '23 at 13:44
  • I'm not sure if I understood. Do you mean adding extra arguments? Like [this](https://tsplay.dev/WGX2Xw)? – wonderflame Aug 21 '23 at 13:46
  • how would you turn args object (in function line 56) into something like this `{ aBool, aString, bBool, cString }` and use them within your function, this gives me error in typescript – amdev Aug 21 '23 at 13:52
  • now typescript cannot pluck out arguments passed based on flags because it does not know which flag is passed to infer related types (which happens in run time ) – amdev Aug 21 '23 at 13:54
  • I got what you meant. But it is expected since it is generic and has no fixed flags. – wonderflame Aug 21 '23 at 14:00
  • then let's imagine i want to do something within my function based on an argument that flagA makes me required to pass -> how can i do it in a typescript friendly way and not getting the current errors when extracting those kind of arguments from `args` object? – amdev Aug 21 '23 at 14:02
  • @amdev can you include a playground with the desired behavior and suggested changes? – wonderflame Aug 21 '23 at 14:56