Last active
November 6, 2025 09:20
-
-
Save jnicklas/afa6a1b1084badc47f57dc5b142b9cb5 to your computer and use it in GitHub Desktop.
Discriminated unions in GTS components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Component from '@glimmer/component'; | |
| import { join } from '@nullvoxpopuli/ember-composable-helpers'; | |
| type SelectArgs = { | |
| options: string[]; | |
| } & ( | |
| | { multiple: true; selected: string[] } | |
| | { multiple?: false; selected: string } | |
| ); | |
| // This component handles both single and multiple selection based on the | |
| // 'multiple' arg using a discriminated union type for the args. | |
| export class SelectComponent extends Component<SelectArgs> { | |
| <template>{{join @options}}</template> | |
| } | |
| // This component just delegates to SelectComponent, passing through all args. | |
| // But implementing this in TypeScript is tricky, even if the args are the exact | |
| // same type! | |
| export class FancySelectComponent extends Component<SelectArgs> { | |
| <template> | |
| I am a fancy select: | |
| <SelectComponent | |
| @options={{@options}} | |
| @selected={{@selected}} | |
| @multiple={{@multiple}} | |
| /> | |
| </template> | |
| } | |
| // TypeScript needs to be able to narrow the type of `@selected`. Here's a brute force way | |
| // of doing this. This is obviously a pretty dumb solution, but this is accepted by TypeScript! | |
| export class FancySelectComponent extends Component<SelectArgs> { | |
| <template> | |
| I am a fancy select: | |
| {{#if @multiple}} | |
| <SelectComponent | |
| @options={{@options}} | |
| @selected={{@selected}} | |
| @multiple={{@multiple}} | |
| /> | |
| {{else}} | |
| <SelectComponent | |
| @options={{@options}} | |
| @selected={{@selected}} | |
| @multiple={{@multiple}} | |
| /> | |
| {{/if}} | |
| </template> | |
| } | |
| // Here's an example using just plain TypeScript without GTS: | |
| export function select(args: SelectArgs) { | |
| // eslint-disable-next-line no-console | |
| console.log(args); | |
| } | |
| // We can do this just fine, but it is not possible to do this with GTS components: | |
| export function fancySelect(args: SelectArgs) { | |
| select(args); | |
| } | |
| // The GTS equivalent of the above function would be more similar to this. Even though | |
| // this looks almost the same, it is a type error in TypeScript, because the relationship | |
| // between multiple and selected is lost. | |
| export function fancySelectGTS({ options, selected, multiple }: SelectArgs) { | |
| select({ options, selected, multiple }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment