Two approaches for dealing with union types:
Given a union type in this fake scenario:
type CommandA = {
id: 'A',
uniqueToA: 'uniqueToA'
}
type CommandB = {
id: 'B',
uniqueToB: 'uniqueToB'
}
type Command = CommandA | CommandB;
function handleCommand(command: Command) {
console.log(command.uniqueToB);
}You’ll get a TS error on uniqueToB because both commands do not implement this attribute. In this contrived scenario you could just accept a CommandB instead of a command, but just roll with it the way it is for now.
Type Guard
function isCommandB(command: Command): command is CommandB {
return command.id === 'B';
}Type Assertion
Source: https://blog.logrocket.com/assertion-functions-typescript
function assertCommand<T extends Command>(
isOfKind: (command: Command) => command is T,
command: Command,
): asserts command is T {
if (!isOfKind(command)) {
throw new Error(`Command is not the asserted type`);
}
}In use:
function handleCommand(command: Command) {
assertCommand(isCommandB, command);
console.log(command.uniqueToB);
}No more TS errors! Assertions let us use the type inline from then on.
Type Narrowing (type predicates?)
function narrowCommand <T extends Command>(
isOfKind: (command: Command) => command is T,
command: Command,
): command is T {
return !!command && isOfKind(command);
}In use:
function handleCommandWithNarrowing(command: Command) {
if (narrowCommand(isCommandB, command)) {
console.log(command.uniqueToB);
}
}No more type error!
Conclusion: TS Playground
I think the main reason you’d prefer type narrowing in production code is you don’t want to throw an error on type mismatch. If a type is not what you’re expecting, you should handle it gracefully and Don’t use errors as control flow where possible.
That said type assertion seems pretty great for writing clearer tests where you do want errors to blow things up as soon as they go wrong!