Dmitry Minkovsky
Dmitry Minkovsky

Reputation: 38143

How do I assign a value to a conditional type?

I'm trying to write a function such that the first parameter is boolean, and depending on whether this argument is true or false, the second argument is a function that accepts either a string or string[].

Here is my attempt:

type P<B extends boolean> = B extends true ? string[] : string

function callback<B extends boolean>(b: B, t: (f: P<B>) => void) {
    const file = 'file'
    if (b) {
        t([file]) // <-- Error: Argument of type 'string' is not assignable to parameter of type 'P<B>'.
    } else {
        t(file)   // <-- Error: Argument of type 'string[]' is not assignable to parameter of type 'P<B>'.
    }
    
}

callback(false, (f: string) => {})  // <-- No problem, resolves the correct argument type
callback(true, (f: string[]) => {}) // <-- No problem, resolves the correct argument type

This works for resolving the correct argument types when the function is called. However, inside the function, the TS compiler is giving me an error that it cannot resolve the conditional type to either string or string[]. What is the correct way to do this?

Playground link.

Upvotes: 9

Views: 1528

Answers (3)

Krzysztof Krzeszewski
Krzysztof Krzeszewski

Reputation: 6749

You can narrow a type down based on another variable by writing custom typings, for instance

function instanceOfMyTYpe(textArray: string[] | string, flag: boolean): textArray is string {
    return flag;
}

let file = "test" as string | string[];
file.toUpperCase(); // doesn't work. Property 'toUpperCase' does not exist on type 'string | string[]'.
if (instanceOfMyTYpe(file, true)) file.toUpperCase(); // but this works
else file.forEach((text)=>text.toUpperCase()); // and so does this

It will not link one variable with another, so invalid function calls can stil happen. For instance calling the function with an array and true flag, will make your program think that the array is actually a string, even though it isn't so be careful.

Upvotes: 2

crashmstr
crashmstr

Reputation: 28573

Here is one way to do it by passing an object with both parameters. By using a type that has true and the one signature or false and the other, the compiler can differentiate the object by testing the b property.

type f = { b: false, t: (f: string) => void };
type t = { b: true, t: (f: string[]) => void };
type fOrT = f | t;

function callback(x: fOrT) {
    const file = 'file'
    if (x.b) {
        x.t([file])
    } else {
        x.t(file)
    }
    
}

callback({ b: false, t: (f: string) => {} })
callback({ b: true, t: (f: string[]) => {} })

TypeScript Playground

Upvotes: 3

jabuj
jabuj

Reputation: 3639

Using generic type arguments like this is a real pain in the neck, I'm not sure how this works exactly, but you can almost never make typescript narrow down the types. If you hover over t inside the if construct, you will see, that it has type (f: P<B>) => void, not (f: string) => void or (f: string[]) => void: it wasn't able to narrow down the type of one variable, depending on another. I think this is a limitation and I can't think of any way to fix this for now. I may be wrong, but I've encountered this situation before in a bit more complex context and had to make changes to the design of the function to make it work.

I think in this case you can just do t([file] as any) and t(file as any), finally, the point of these types is to force calling function in the correct way. If it's called correctly, then inside it knows what to do, I think it's worth adding a couple of anys here.

Also you could use overloads to get rid of generics, but it doesn't solve the issue:

function callback(b: true, t: (f: string[]) => void): void
function callback(b: false, t: (f: string) => void): void
function callback(b: boolean, t: ((f: string) => void)) | ((f: string[]) => void)) {
  //
}

One solution that would actually not need anys is to use objects, but this is not ideal, because you may need to change other logic in your application:

type Options = {
  b: true
  t: (f: string[]) => void
} | {
  b: false
  t: (f: string) => void
}

function callback(options: Options) {
  const file = 'file'
  if(options.b) options.t([file])
  else options.t(file)

Upvotes: 1

Related Questions