typescriptmapped-typesconditional-types

Why I'm getting error indexing in a conditional when mapping an object type


I'm adding a new property value:"4" to the object Foo, I have two approches in aaa and bbb. When hovering over them, both show the same expected result:{ name:string; value:"4";} but there is an error in type bbb saying: Type 'k' cannot be used to index type 'Foo'

type Foo={
    name:string
}

type aaa={
    [k in keyof Foo|"value"]:k extends keyof Foo?Foo[k]:"4"
}

type bbb={
    [k in keyof Foo|"value"]:k extends "value"?"4":Foo[k]
                                                   ^^^^^^
}

Why the error? Is it that the conditional in aaa being true constrains k so that Foo[k] become valid?


Solution

  • This is a missing feature of TypeScript, requested at microsoft/TypeScript#48710. If you have a generic type X which is constrained to another type Y, then a conditional type like X extends Z ? T<X> : F<X> the type of X is re-constrained to Y & Z in the true branch T<X>, but it is not re-constrained to something like "Y but not Z" in the false branch.

    In general it would not be correct to do so, since there are situations in which X extends Y is true, X extends Z is false, but X extends Y-but-not-Z is also false. For example:

    type F<X extends { x: string | number }> =
        X extends { x: string } ? X['x']['toUpperCase'] : 
        X['x']['toFixed']; // <-- error!
    

    That's flagged as an error for good reason; because you could evaluate something like:

    type G = F<{ x: 0 | "a" }>
    

    where {x: 0 | "a"} extends {x: string | number} is true, and {x: 0 | "a"} extends {x: string} is false, but {x: 0 | "a"} extends {x: number} is also false.

    So it's not considered a bug to fail to re-constrain the false branch, because it's not always warranted.


    Still, in situations like this where the generic type is constrained to a union of literal types and you are checking it against some subset of that union, and the generic type is only ever going to be a single member of that union (e.g., it's a distributive conditional type or you are iterating over a set of known keys in a mapped type), then it would be safe to do it. It just hasn't been implemented. So it's a missing feature.

    For now the workarounds are to reorder your conditional type as you've shown above, or do a possibly redundant second check:

    type BBB = {
        [K in keyof Foo | "value"]:
          K extends "value" ? "4" : 
          K extends keyof Foo ? Foo[K] : never;
    }
    

    Playground link to code