1

I'm sorry for the vague title, but my question here comes from not really understanding what TS is doing in this situation so I wasn't sure what to call it. Basically, the question is--why does this code fail:

type Broken<T extends string | number> = T extends string ? T : null

let foo:Broken<string>
let value: string 
foo = value

let value2: string | number
// this error makes sense to me: value2 could be a number
foo = value2
if(typeof value2 === "string")
{
    // and it makes sense to me this works, because now we know they are both strings
    foo = value2
}

function testBroken<T extends string | number>(value: T ){
    let bar: Broken<T>
    // i guess i expect this error, but i'm not sure i understand why TS says the error
    // is that "string" is not assignable to Broken<T>
    bar = value
    if(typeof value === "string"){
        // Why is this an error? if "bar" is narrowed to string don't we know that Broken<T>
        // is also "string"?
        bar = value
    }
}

Playground here

More specifically, why do the assignments to bar generate errors (especially the second one) but not the second assignment to foo. I think it has something to do with type distribution in conditional types, but I can't really wrap my head around it.

Can someone explain what is going on here? (Note: This question is more about understanding what TS is doing here, rather than coming up with a workaround for the typing).

sam256
  • 1,291
  • 5
  • 29

1 Answers1

2

This is currently a design limitation of TypeScript. The compiler defers evaluation of conditional types that depend on an as-yet-unspecified generic type parameters, and thus it cannot really verify that anything is assignable to such types. In your case, Broken<T> is a conditional type, and inside the implementation of testBroken(), the generic type parameter T on which it depends has not been specified (it only gets specified when you call testBroken()). So Broken<T> is essentially opaque to the compiler and it cannot see that value is assignable to it, even if you could narrow T or value to a subtype of string.

Probably the canonical issue in GitHub surrounding this is microsoft/TypeScript#33912. It suggests that the compiler should use control flow analysis to narrow value and thus possibly T to a subtype of string, and then use that to narrow Broken<T> to T. If you want to see this happen you might want to go to that issue and give it a . Until and unless this is supported, there are just workarounds.

Since the question is not about workarounds, I won't go into much detail, but briefly you can always use type assertions to quell the warnings:

function testBrokenAssertion<T extends string | number>(value: T) {
    let bar: Broken<T>
    if (typeof value === "string") {
        bar = value as Broken<T>
    }
}

and you can even refactor these assertions into a helper function as shown in microsoft/TypeScript#33912:

function testBrokenHelper<T extends string | number>(value: T) {
  let bar: Broken<T>
  bar = conditionalProducingIf(value, isString, () => value, () => null) // okay
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • this was a really helpful answer. thank you. so basically, if you pass a generic to a conditional type the only thing you can then assign to that generic-conditional-type is that exact generic-conditional-type. so if your code has arrived at a narrower type you have to widen by assertion (whether explicitly or through a helper function). – sam256 Jun 27 '21 at 13:46