Search code examples
typescript

Type narrowing doesn't work in a switch statement


Why does the following code not typecheck (TypeScript 5.5.4):

    type ValidationResult$Type = 'success' | 'invalid_or_expired_token' 
    export type ValidationResult = {
      type: 'success'
      token_info: object
    } | {
      type: 'invalid_or_expired_token'
    };
    
    const x: ValidationResult = undefined as unknown as ValidationResult;
    
    const {type}: {type: ValidationResult$Type} = x;
    
    switch (type) {
        case 'success':
        const {token_info} = x;
        break;
        case 'invalid_or_expired_token':
        break;
    }

while the below, simpler, code does:

// type ValidationResult$Type = 'success' | 'invalid_or_expired_token' 
export type ValidationResult = {
  type: 'success'
  token_info: object
} | {
  type: 'invalid_or_expired_token'
};

const x: ValidationResult = undefined as unknown as ValidationResult;

// const {type}: {type: ValidationResult$Type} = x;
const {type} = x;

switch (type) {
    case 'success':
    const {token_info} = x;
    break;
    case 'invalid_or_expired_token':
    break;
}

Playground


Solution

  • In this demo you can see the two cases together:

    A)

    function f1() {
      const {type} = x;
      switch (type) { // type is "success" | "invalid_or_expired_token"
          case 'success':
            const test1 = x.token_info; // OK
            break;
          case 'invalid_or_expired_token':
            const test2 = x.token_info; // Error
            break;
      }
    }
    

    In this case, type is "success" | "invalid_or_expired_token". When TypeScript is processing the control flow analysis, it is able to see that type comes from x.type, then TS is smart enough to guess that if x.type is "success", then x has the property token_info.

    B)

    function f2() {
      type ValidationResult$Type = ValidationResult['type'];
      const {type}: {type: ValidationResult$Type} = x;
      switch (type) { // type is "success" | "invalid_or_expired_token"
          case 'success':
            const test1 = x.token_info; // Error
            break;
          case 'invalid_or_expired_token':
            const test2 = x.token_info; // Error
            break;
      }
    }
    

    In this case, type is also"success" | "invalid_or_expired_token". When TypeScript is processing the control flow analysis, it is NOT able to see that type comes from x.type. Your annotation {type: ValidationResult$Type} takes precedence over inferring it from x, then TS has no way to connect the dots and guess that if type is "success", then x has the property token_info.

    The moral of the story is that you should avoid adding manual type annotation whenever possible.