Vishwesh Jainkuniya
Vishwesh Jainkuniya

Reputation: 2839

typescript: Derive types from Conditional Types

Typescript supports Conditional Types. But when I try to set value of op as string, it gives me error, how do I check the type and assign value?

export const t = <IsMulti extends boolean = false>(): void => {
    const value = 'test';
    type S = string;
    type D = IsMulti extends true ? S[] : S;
    const op: D = value;
    console.log(op);
};

Error: Type 'string' is not assignable to type 'D'.

I have even tried adding a parameter of same type IsMulti and adding check based on it

export const t = <IsMulti extends boolean = false>(isMulti: IsMulti): void => {
    const value = 'test';
    type S = string;
    type D = IsMulti extends true ? S[] : S;
    if (!isMulti) {
        const op: D = value;
        console.log(op);
    }
};

still it gives the same error. enter image description here

Upvotes: 4

Views: 283

Answers (1)

jcalz
jcalz

Reputation: 328262

Evaluation of conditional types which depend on as-yet unspecified generic type parameters is generally deferred by the compiler; if the compiler doesn't know exactly what IsMulti is, then it does not know exactly what IsMulti extends true ? S[] : S is, and so it won't let you assign a value of type (say) S to that conditional type, because it is unable to verify that this is a type-safe operation. The compiler doesn't even really try to evaluate such conditional types; it leaves them as opaque things that almost nothing can be assigned to.

For your first example this is the desired behavior from the compiler; it really isn't safe to assign a string to op because D may well be string[]. Nothing stops someone from calling t<true>(). Just because IsMulti has a default of false doesn't mean it is false.


For your second example, this is currently a limitation of TypeScript. The compiler cannot use control flow analysis to narrow type parameters. And therefore it cannot verify assignability to a See microsoft/TypeScript#33912 for details.

When you check if (!isMulti), the compiler narrows the type of the isMulti inside the subsequent code block from the IsMulti type to just false:

export const t = <IsMulti extends boolean = false>(isMulti: IsMulti): void => {
  if (!isMulti) {
    // isMulti is now known to be false 
    const fls: false = isMulti; // okay
    const tru: true = isMulti; // error
  }
};

But it does not narrow the type parameter IsMulti itself. In general this is also desired behavior from the compiler, since IsMulti extends boolean implies that IsMulti might actually be the full union boolean (as opposed to just true or just false) and so checking a value of type IsMulti would not be useful to narrow IsMulti itself. All you could say with if (!isMulti) is that IsMulti could not be just true... but it might still be boolean. Of course if it is boolean, then D, a distributive conditional type would become the union S[] | S, and you should be allowed to assign a string to that. So there's no plausible way that assigning "test" to op should be unsafe.

And yet, the compiler is unable to verify this. It stubbornly leaves IsMulti alone, and won't let you assign anything to D because it defers evaluation. It would be nice if there were some supported way to narrow type parameters, or to verify assignability to generic conditional types. For now, there really isn't. The above linked GitHub issue is a feature request to improve this, and there are a bunch of related requests that might help (e.g., microsoft/TypeScript#27808 could restrict IsMulti to be exactly true or exactly false, and presumably then if (!isMulti) would narrow the previously generic type IsMulti to the specific type false, and D would be evaluated eagerly). You could go to these issues and give them a 👍, but it's not obvious that anything is going to happen here anytime soon.


Instead, in situations like this where you know more about the types than the compiler does, you can use a type assertion to suppress assignability errors:

export const t = <IsMulti extends boolean = false>(isMulti: IsMulti): void => {
  const value = 'test';
  type S = string;
  type D = IsMulti extends true ? S[] : S;
  if (!isMulti) {
    const op = value as D; // okay
    console.log(op);
  }
};

By writing value as D you're telling the compiler that you know that value is definitely of type D, even though the compiler cannot. This allows the program to compile without error. Note that this is not magic, and by doing a type assertion you are taking some of the responsibility for type safety away from the compiler. If you have made a mistake or lied to the compiler, it can't always catch it:

if (!isMulti) {
  const op = ["oopsie"] as D; // still okay
  console.log(op);
}

So be careful!

Playground link to code

Upvotes: 3

Related Questions