Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discriminated Union Return Type #60467

Closed
alanbacon opened this issue Nov 10, 2024 · 3 comments
Closed

Discriminated Union Return Type #60467

alanbacon opened this issue Nov 10, 2024 · 3 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@alanbacon
Copy link

πŸ”Ž 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,
});
@jcalz
Copy link
Contributor

jcalz commented Nov 10, 2024

ClassifierType<'fooString'> | ClassifierType<'barNumber'> is just string | number. That's not a discriminated union, it's just a union. You are expecting TS to perform a sort of narrowing of generic type parameters that it doesn't do... well, not the way you want. Until recently I'd have said you want #33912 or #33014, but those have been closed as fixed by #56941. Unfortunately what you're doing is one of the "unsupported scenarios" from that (e.g., // Property type).

For now, you're sort of hitting #30581 and the supported approach to that is described in #47109. Your example would be refactored to

interface ClassifierMap {
  fooString: string;
  barNumber: number;
}

type GenericType<K extends keyof ClassifierMap> = { [P in K]: {
  classifier: P;
  value: ClassifierMap[P];
} }[K]


function genericFunction<K extends keyof ClassifierMap>(
  input: GenericType<K>
): ClassifierMap[K] {
  const m: { [P in keyof ClassifierMap]: (input: GenericType<P>) => ClassifierMap[P] } = {
    fooString(input) { return input.value[0] },
    barNumber(input) { return input.value + 1 }
  };
  return m[input.classifier](input);
}

const val = genericFunction({
  classifier: 'fooString',
  value: 'foo',
});

which compiles and behaves as desired.
Playground link to code

Maybe we can open a new version of #33014/#33912 for the unsupported cases not handled by #56941, and then this would be lumped in there?

@alanbacon
Copy link
Author

@jcalz Thank you so much for the links, explanation and reworked example.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Nov 12, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Nov 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants