Skip to content

Discriminated Union Return TypeΒ #60467

Closed as not planned
Closed as not planned
@alanbacon

Description

@alanbacon

πŸ”Ž Search Terms

discriminated union return type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about discrimintated union return types

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.3#code/C4TwDgpgBAwgNgQwM5IJYDNUQE5QLxQDk6A9iQMrDaoB2A5oVAD5EBGC2AcgK4C2rOQgG4AUCNCRYiFBizYAKuAgAeeVAgAPYBBoATJFORpMOAHz4oazdr0HiZStXqERUKAH4oSKrTquoAFxQNHwC2KLiSlAA4jo4qADGipCq6lo6+oYyJtjmBADe-gnSxnJB8qJuAG4IcNwQQfBGsjjJKvKmogC+ERLQsTTxSUoAsghgFoVupBQ+9EEDQ23K9rNODJ3+7FyhOAtx1MMphNs8-IKbPWIA9NdQ3EjQwAAWqAbAJFAJ2BAI2lAIKB9AFQXRvb6oXi0P4QXT3GioEg0faDQ7LVaOXyEcwsRZopQrU67bDYkS3AEGEhgMAkR5wj4gmhIgC0YKQCUhtGAMLh3ARSJiB0S6JmmOczDYHDOYVJwLxwqUAFV+TRUtYMgYmtk5HkoFMoABtADSUFolgAuiilqNxsbzd0DfJ7WJgVrSq0bRMCv5jaaaFl3dhLYLUQqUkbTAbCDU6hBCM6rmS7g8nq93p9vr9-oDgYC2RCoTQefDEciAy0FASMXMNhK3RX0UTziTTEmKVAqTS6UDPoCmTRWeDOTRudpeSryzkRQ4a4wWCcpcTZVF61OlSq1elbJOdZMfSazU7GiUG567Q6nRF0HyEsBS1A6EKEgAxG93pGbmyZVc6gAU-gAfWKZocnKAAaKByUGWEeygMAjBBXQ+F4EAvhPHIoBqXAGWeCA4AmF4niUdlqDAYAvhIXgwFQOAcAggB3CBUGwOFWG4cimXI+jnj+IE0ygH4aWwci3gBVgSHY-xaDAdirXxSBlVLVRWwASmPEC5DaRSPw6PUpPQKBf2k9iADpgO1HB8DwAhq3WQgVL0tw3B+YBuGwf1jOAEyY3qA0AAZnVuEyoAAPTC8KoAAUQAJWigB5aKgm8dZTQMTiKTQOgi1YWjYOBQYqhwfwunUOBHkcpyXLcjyaBkryfOgABqKAAEZRCC0LwrCqLYoSoIQmbVLghIcjmiyhAcqeT58ogQrsGKkQujEBIkW8TDagsR9QxfN9S1-WysQgwoOrcCK2JEgwVuwH5bzgVDaHQHAfjhZAQWS3woFwn4inQsoiFFWcwP8BqglWQgga6FShCAA

πŸ’» Code

type Classifier = 'fooString' | 'barNumber';

type ClassifierType<T extends Classifier> = T extends 'fooString'
  ? string
  : number;

type GenericType<T extends Classifier> = {
  classifier: T;
  value: ClassifierType<T>;
};

type GenericTypeMap = {
  fooString: GenericType<'fooString'>;
  barNumber: GenericType<'barNumber'>;
};

// use this to create a type a discriminated union: GenericType<'fooString'> | GenericType<'barNumber'>
// as opposed to a non-discimintated union GenericType<'fooString' | 'barNumber'>
type GenericTypeUnion<T extends Classifier> = {
  [K in T]: GenericTypeMap[K];
}[T];

type ClassifierTypeMap = {
  [K in Classifier]: GenericType<K>['value'];
};

// use this to create a type a discriminated union: ClassifierType<'fooString'> | ClassifierType<'barNumber'>
// as opposed to a non-discimintated union ClassifierType<'fooString' | 'barNumber'>
type ClassifierTypeUnion<T extends Classifier> = {
  [K in T]: ClassifierTypeMap[K];
}[T];

function genericFunction<T extends Classifier>(
  _classifier: T, // need to pass a dummy classifier var to help the typescript compiler, weird but not what this report is about
  input: GenericTypeUnion<T>
): ClassifierTypeUnion<T> {
  if (input.classifier === 'fooString') {
    return input.value[0];
//. ^^^^^^ ERROR: string is not assignable to type never
  } else {
    return input.value + 1;
//. ^^^^^^ ERROR: number is not assignable to type never
  }
}

const val = genericFunction('fooString', {
//.   ^^^ but is correctly inferred as a string here
  classifier: 'fooString',
  value: 'foo',
});

πŸ™ Actual behavior

When calling genericFunction the return type is correctly infered as a string (or number depending on the input).

However inside the function itself: the type checking is not working for return types

πŸ™‚ Expected behavior

The type checking should be consistent between the place where the function is called and where the actual function return is defined.

Additional information about the issue

All type checking works as expected where the function is called.

e.g.

// TS knows that the input type (second arg) is not matching the classifier type (first argument)
// TS has inferred that the return type should be a number based on the classifer type (first argument)
const val = genericFunction('barNumber', {
  classifier: 'fooString',
  value: 'foo',
});

and

// TS knows that the input type value is not matching the classifier type 
const val = genericFunction3('fooString', {
  classifier: 'fooString',
  value: 9,
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions