Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I assign a value to a conditional type?

Tags:

typescript

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.

like image 674
Dmitry Minkovsky Avatar asked Nov 18 '20 13:11

Dmitry Minkovsky


People also ask

How do you add a condition in TypeScript?

Javascript. type Conditional<G> = G extends { typeof : number|string|Boolean} ? Example 3: In this example first, we will create the conditional types for the number and string in one conditional type. After that, we use the same conditional types for both string and number.

What are conditional types How do you create them?

Conditional types help describe the relation between the types of inputs and outputs. When the type on the left of the extends is assignable to the one on the right, then you'll get the type in the first branch (the “true” branch); otherwise you'll get the type in the latter branch (the “false” branch).

What does ?: Mean in TypeScript?

Using ?: with undefined as type definition While there are no errors with this interface definition, it is inferred the property value could undefined without explicitly defining the property type as undefined . In case the middleName property doesn't get a value, by default, its value will be undefined .

How do I know my TS type?

Use the typeof operator to check the type of a variable in TypeScript, e.g. if (typeof myVar === 'string') {} . The typeof operator returns a string that indicates the type of the value and can be used as a type guard in TypeScript.


Video Answer


3 Answers

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

like image 85
crashmstr Avatar answered Nov 15 '22 05:11

crashmstr


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.

like image 22
Krzysztof Krzeszewski Avatar answered Nov 15 '22 04:11

Krzysztof Krzeszewski


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)
like image 45
Alex Chashin Avatar answered Nov 15 '22 05:11

Alex Chashin