microsoft / TypeScript

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

[TS 4.2] Narrowing a union type is inconsistent using type guards on different member of union #43904

Open zen0wu opened 3 years ago

zen0wu commented 3 years ago

Bug Report

🔎 Search Terms

narrowing union type

🕗 Version & Regression Information

⏯ Playground Link

Playground link with relevant code

💻 Code

export type SuccessStatusCode = 200 | 201
export type FailureStatusCode = 400 | 401 | 402 | 403 | 500 | 502

interface HttpResponse<StatusCode extends SuccessStatusCode | FailureStatusCode> {
  statusCode: StatusCode
}

interface LegacyApiResponse extends Partial<{ [key: string]: unknown }> {
    statusCode?: never
}

type Success = HttpResponse<SuccessStatusCode> | LegacyApiResponse
type Failure = HttpResponse<FailureStatusCode>

type Result<S extends Success, F extends Failure = never> = S | F

declare function resultIsSuccess<S extends Success, F extends Failure>(r: Result<S, F>): r is S
declare function resultIsFailure<S extends Success, F extends Failure>(r: Result<S, F>): r is F

type TestResult = Result<{ success: true }, HttpResponse<403>>

declare const result: TestResult

export function test() {
  if (resultIsFailure(result)) {
    result // Inferred as HttpResponse<403>.
  } else {
    result // Inferred as { success: true }.
  }
}

export function test2() {
  if (resultIsSuccess(result)) {
    result // Inferred as { success: true }.
  } else {
    result // Cannot infer, stay as TestResult.
  }
}

🙁 Actual behavior

While test1 works fine, in test2, the else branch cannot narrow the type to HttpResponse<403>.

🙂 Expected behavior

test1 and test2 are just narrowing down result using complementary type guards, their behavior should be consistent. And they are consistent in 4.1.

RyanCavanaugh commented 3 years ago

There's no reason to have S and F in both functions; you can write

declare function resultIsSuccess<S extends Success>(r: Result<S, Failure>): r is S
declare function resultIsFailure<F extends Failure>(r: Result<Success, F>): r is F

which works as expected