A TypeScript utility for destructuring discriminated unions while maintaining type safety. This package is tiny (no dependencies) and solves a common problem in Typescript. Broadly, this package helps with:
- Safely destructuring discriminated unions
- Making switch/case statements exhaustive
- Improving state management patterns
- Handling API responses more safely
- Reducing boilerplate type guards
npm install ts-safe-unionIn TypeScript, when working with discriminated unions, you normally can't destructure properties that don't exist on all members of the union:
// Before: Normal TypeScript union
type StateA = { state: "A"; A: number };
type StateB = { state: "B"; B: string };
type State = StateA | StateB;
// Error: Property 'B' does not exist on type 'StateA'
const { state, A, B } = someState;Helpfully, ts-safe-union was designed to improve this behavior:
// After: Using DiscriminatedUnion
type State = DiscriminatedUnion<
"state",
{
A: { A: number };
B: { B: string };
}
>;
// Works! Properties are safely destructurable
const { state, A, B } = someState;Use DiscriminatedUnion when you have a well-defined discriminating property (e.g. status, type, etc.) or you have many states.
import { DiscriminatedUnion } from "ts-safe-union";
// Define your discriminated union type
type RequestState = DiscriminatedUnion<
"status", // The discriminator property name
{
// Each key defines a variant with its properties
loading: { progress: number };
success: { data: unknown };
error: { error: Error };
}
>;
// Example request consumer
const handleRequest = (request: RequestStateWithCommon) => {
const { status, progress, data, error } = request;
if (status === "loading") {
console.log(`Loading: ${progress}%`);
} else if (status === "success") {
console.log(`Success: ${JSON.stringify(data)}`);
} else {
console.log(`Error: ${error.message}`);
}
};If you want to define your keys elsewhere, you can provide them to the Discriminated Union and the TS complier will give you errors if some variant are not supplied in the list. For example, the following example will have a type error because the error variant is not specified.
import { DiscriminatedUnion } from "ts-safe-union";
const statusKeys = ["loading", "success", "error"] as const;
type RequestState = DiscriminatedUnion<
"status",
{
loading: { progress: number };
success: { data: unknown };
},
typeof statusKeys[number] // << error here
>;Use MergedUnion when you want to merge two existing object types into a single union while maintaining safe property access.
import { MergedUnion } from "ts-safe-union";
// Define your individual state types
type Success = { state: "success"; data: unknown };
type Error = { state: "error"; error: Error };
// Merge them into a union type
type RequestState = MergedUnion<Success, Error>;
const handleRequest = (request: RequestState) => {
const { state, data, error } = request;
if (state === "success") {
console.log(`Success: ${JSON.stringify(data)}`);
} else {
console.log(`Error: ${error.message}`);
}
};Check out the examples directory for simple but practical use cases:
- Discriminated Union Example - Authentication state management
- Merged Union Example - Task processing state
Contributions are welcome! Please feel free to submit a PR. If you feel comfortable, also:
- Add tests for any new features
- Update documentation if needed
- Ensure all tests pass by running
npm test
MIT © Jomity