Discriminated Unions and converting types
09:09 20 Apr 2026

I have a function that is used to change the type of an API input (simple dummy example given):

interface BaseInput {
  version: '3' | '4';
}

interface InputA extends BaseInput{
  version: '4';
  foo: string;
}

interface InputB extends BaseInput{
  version: '3';
  bar: string;
}

const convert3to4 = (apiBodyInput: InputB) => {
  return { version: '4', foo: apiBodyInput.bar } as InputA;
};

const convert4to3 = (apiBodyInput: InputA) => {
  return { version: '3', bar: apiBodyInput.foo } as InputA;
};

// adapted from https://dev.to/maikelev/discriminated-unions-in-typescript-5fia#exhaustiveness-checking

function assertNever(version: never): never {
  throw new Error(`Unrecognised api version: ${version}`);
}

export const changeApiVersion(apiBodyInput: InputA | InputB, targetVersion: '3' | '4') => {
  if(apiBodyInput.version !== '3' && apiBodyInput.version !== '4'){
    return assertNever(apiBodyInput.version)
  }
  switch(targetVersion) {
    case '4':
      return apiBodyInput.version === '3' ? convert3to4(apiBodyInput) : apiBodyInput;
    case '3':
      return apiBodyInput.version === '4' ? convert4to3(apiBodyInput) : apiBodyInput;
    default:
      return assertNever(targetVersion);
  }
}

Hovering over each branch of the switch statement, TS knows each return can only be of one type. But when I use it (e.g. changeApiVersion({version: 3, bar: 'sad clown noise'}, 4) or similar) I get back the output is InputA | InputB

If I flip the logic to check apiBodyInput.version in the switch case, and the target version in the if/else I get the same issue (albeit now each case has a varied output), e.g.:

export const changeApiVersion(apiBodyInput: InputA | InputB, targetVersion: '3' | '4') => {
  if(targetVersion !== '3' && targetVersion !== '4'){
    return assertNever(targetVersion)
  }
  switch(apiBodyInput.version) {
    case '3':
      return targetVersion === '4' ? convert3to4(apiBodyInput) : apiBodyInput;
    case '4':
      return targetVersion === '3' ? convert4to3(apiBodyInput) : apiBodyInput;
    default:
      return assertNever(apiBodyInput.version);
  }
}

Either case it doesn't seem to be narrowing down my types.

Where am I going wrong?

javascript typescript discriminated-union