microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.33k stars 12.54k forks source link

TypeScript does not narrow union type with more than 25 possible members #32749

Closed kasperisager closed 5 years ago

kasperisager commented 5 years ago

TypeScript Version: 3.5.1

Search Terms:

25 union members, narrow union

Code

export interface T1 { type: "t1" }
export interface T2 { type: "t2" }
export interface T3 { type: "t3" }
export interface T4 { type: "t4" }
export interface T5 { type: "t5" }
export interface T6 { type: "t6" }
export interface T7 { type: "t7" }
export interface T8 { type: "t8" }
export interface T9 { type: "t9" }
export interface T10 { type: "t10" }
export interface T11 { type: "t11" }
export interface T12 { type: "t12" }
export interface T13 { type: "t13" }
export interface T14 { type: "t14" }
export interface T15 { type: "t15" }
export interface T16 { type: "t16" }
export interface T17 { type: "t17" }
export interface T18 { type: "t18" }
export interface T19 { type: "t19" }
export interface T20 { type: "t20" }
export interface T21 { type: "t21" }
export interface T22 { type: "t22" }
export interface T23 { type: "t23" }
export interface T24 { type: "t24" }
export interface T25 { type: "t25" }
export interface T26 { type: "t26" }
export interface T27 { type: "t27"; value: number }

export type T = T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10 | T11 | T12 | T13 | T14 | T15 | T16 | T17 | T18 | T19 | T20 | T21 | T22 | T23 | T24 | T25 | T26 | T27;

function make(type: string): T | null {
    switch (type) {
        case "t1":
        case "t2":
        case "t3":
        case "t4":
        case "t5":
        case "t6":
        case "t7":
        case "t8":
        case "t9":
        case "t10":
        case "t11":
        case "t12":
        case "t13":
        case "t14":
        case "t15":
        case "t16":
        case "t17":
        case "t18":
        case "t19":
        case "t20":
        case "t21":
        case "t22":
        case "t23":
        case "t24":
        case "t25":
        case "t26":
            return { type };
    }

    return null;
}

Expected behavior:

I would expect { type } to be narrowed to T1 | T2 | ... | T26 and the code compile.

Actual behavior:

TypeScript instead infers { type } to be T and fails to compile as T27 also requires a value property. However, if any of the cases are removed in order to bring the number of union type members down to 25, TypeScript correctly narrows { type } to T1 | T2 | ... | T25 (assuming T26 was removed) and compiles the code.

Playground Link: https://www.typescriptlang.org/play/index.html#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgFQEY4BvOGATzGAC44AiGQ+uAXwChRJZEU0tcBAEylyVWgxhCWHLtHjJUGHHnwBmUZWp1GamZ3DzeSgaoAsm8Tphn9cnov4qCAVkvbJLu4Yd9lg-AA2dwlGQO9uBT9TAgB2EOtYiKNHf1UADgTJdOTfE2d8AE4sxkLcqPyAwgAGEqZq8uMnKuIyLVCmZjYDSKa0gkIRNqtJQcbUmKINYY9GQj1u+wrm1UILGY618eiCwjcN6z3tytXgg9HwxZ9l-qJ487mkq96J3cyHphznlJ2q4o-CGVvnkVsJah8hA1gTdJkJWmJZlIurJrn1YUMER0hNJoWiCkJppjrATjqD8EJ1kTJBTSbchPsqYx6bTYWdGVJLiiXr9VEJ7uy+fQANxwABumAANgBXCRIKUAWwARmhuj0jO0CHAALwDOAAH2E+oIGgN+AsprcpuCpvipsypuKppqRqIxCdIidJoG5oGloG1oGtoG9oGjrBLrhEY9wi95J95L95ID5NiQvY7HQUqQ2BgCAgSDg8swAGtgAAKdp0ADOMCgyAA5gBKOj4I1yiUS0jsOA9uBVgDuCBg2AAFnAK+JG13ezO4NhMFW8HN6DRu7Oe-PF9SV2v15ulzA9Kv1xuFwfbMeT-vPDuT3Oz5Jwpe9w-GEln7Pr4wch+Z1+YGUv69v+NS3ler6dGBL5bnM0hAaeMFMEeu6fhBWzwfeiFHBhIFPihf5oe++HAWhP7EQhB6AlBqGIZC1EEbRzA4RB2L0SRtHIXe-40sxtFeLxB5CHhd4zlAwAwFKUAFhsbBpjOHAoWJElSXA7YSmmrBAA

Related Issues:

RyanCavanaugh commented 5 years ago

This is an intentional cutoff to prevent later steps from becoming too combinatorially explosive.

RyanCavanaugh commented 5 years ago

I should add that prior to 3.4, this didn't even work for n = 2 πŸ˜‰

kasperisager commented 5 years ago

Alright, that seems fair then πŸ˜„ It would be nice with some more transparency around cases like this though, similar to how the compiler complains when union types become too complex. It wasn't immediately obvious why the compiler started complaining when the 26th case was added to the actual code πŸ™ˆ

Thanks for the answer!

RyanCavanaugh commented 5 years ago

I agree it'd be nice. Unfortunately this happens deep inside assignability checking and there might not actually end up being a downstream error when the assignability check fails as in this case, so it'd sometimes issue false positive errors unless we had some more formal causality analysis built in to the typechecking process.