# Streamlined Discriminated Union Decoding in TypeScript with jsonous's New Decoder

TypeScript developers love discriminated unions (or tagged unions). They provide a fantastic way to model states, events, or different kinds of data structures in a type-safe manner. When working with external data sources like JSON APIs, however, decoding these unions reliably can sometimes feel a bit cumbersome.

Here at `jsonous`, we aim to make JSON decoding as painless and type-safe as possible. While our existing `oneOf` decoder could handle unions, it wasn't specifically optimized for the common discriminated union pattern. Today, we're excited to introduce a new tool designed precisely for this job: the `discriminatedUnion` decoder!

### The Challenge: Decoding Discriminated Unions "The Old Way"

Let's consider a common example: representing different types of users in our system.

```typescript
// Our TypeScript types
interface User {
  type: 'user';
  id: string;
  name: string;
  isActive: boolean;
}

interface Admin {
  type: 'admin';
  id: string;
  name: string;
  permissions: string[];
}

type Person = User | Admin;
```

We have a `Person` type which can be either a `User` or an `Admin`, distinguished by the `type` field.

Using `jsonous` previously, you'd typically define decoders for each variant and combine them with `oneOf`:

```typescript
import {
  string,
  boolean,
  array,
  stringLiteral,
  createDecoderFromStructure,
  oneOf,
  Decoder,
  identity // Needed for mapping
} from 'jsonous';

// Decoders for each variant
const userDecoder: Decoder<User> = createDecoderFromStructure({
  type: stringLiteral('user'),
  id: string,
  name: string,
  isActive: boolean,
});

const adminDecoder: Decoder<Admin> = createDecoderFromStructure({
  type: stringLiteral('admin'),
  id: string,
  name: string,
  permissions: array(string),
});

// --- The "Old Way" using oneOf ---
const personDecoderOneOf: Decoder<Person> = oneOf([
  // We need to explicitly map each decoder to the union type
  userDecoder.map<Person>(identity),
  adminDecoder.map<Person>(identity),
]);
```

This works, but has a few drawbacks:

1. **Verbosity:** You need `.map<Person>(identity)` for every single variant. This adds boilerplate, especially with many variants.
    
2. **Less Specific Errors:** If decoding fails, `oneOf` tries *every* decoder in the list and reports *all* failures. For a discriminated union, you often intuitively know *which* variant *should* have matched based on the `type` field, making the other errors noise.
    
3. **Potential Inefficiency:** `oneOf` might run multiple potentially complex decoders even if the `type` field clearly indicates only one is relevant.
    

### Introducing `discriminatedUnion`: The Right Tool for the Job

The new `discriminatedUnion` decoder is designed to address these pain points directly. It leverages the discriminator field (`type` in our case) to intelligently select and run the correct decoder.

**Here's how it works:**

1. You tell it the name of the `discriminatorField` (e.g., `"type"`).
    
2. You provide a `mapping` object where keys are the possible string values of the discriminator (e.g., `"user"`, `"admin"`) and values are the corresponding decoders for each variant.
    
3. It first decodes *only* the discriminator field.
    
4. Based on the value found, it looks up the correct decoder in your mapping.
    
5. It runs *only that specific decoder* on the original input.
    

Let's rewrite our `Person` decoder using `discriminatedUnion`:

```typescript
import {
  // ... other imports remain the same ...
  discriminatedUnion // Import the new decoder
} from 'jsonous';

// userDecoder and adminDecoder definitions remain the same...

// --- The "New Way" using discriminatedUnion ---
const personDecoder: Decoder<Person> = discriminatedUnion('type', {
  user: userDecoder, // Key matches the 'type' value
  admin: adminDecoder, // Key matches the 'type' value
});

// Type check: personDecoder is automatically Decoder<Person> - no mapping needed!
```

Look how much cleaner that is! No more `.map(identity)`. The decoder's type `Decoder<Person>` is inferred automatically from the provided mapping.

### Why `discriminatedUnion` is Better

This new decoder offers significant advantages:

1. **Type Safety:** Automatically infers the correct union type (`User | Admin` in this case) without manual type hints in `.map`.
    
2. **Conciseness:** Eliminates the repetitive `.map(identity)` calls, making your decoder definitions cleaner and easier to read.
    
3. **Clarity:** The structure `discriminatedUnion('field', { key1: decoder1, key2: decoder2 })` clearly expresses the intent of choosing a decoder based on a specific field's value.
    
4. **Targeted Errors:** Error messages are much more helpful. Instead of trying all decoders, it fails fast with specific reasons:
    
    * Did the discriminator field (`type`) exist and was it a string?
        
    * Was the value of the discriminator field (`"user"`, `"admin"`) one of the expected keys in the mapping?
        
    * Did the *selected* variant decoder (`userDecoder` or `adminDecoder`) fail?
        
5. **Efficiency:** It avoids running unnecessary decoders. It only decodes the simple discriminator field first and then runs exactly one variant decoder.
    

### A Clearer Picture: Error Handling

Let's see the difference in error messages. Consider this invalid input:

```typescript
const invalidData = { type: 'guest', id: 'guest-001' };
```

**Error with** `oneOf`:

```plaintext
// personDecoderOneOf.decodeAny(invalidData) might produce:
Err: I found the following problems:
Expected user but got "guest":
occurred in a field named 'type'
Expected admin but got "guest":
occurred in a field named 'type'
```

(It tells you *both* decoders failed because the `type` was wrong).

**Error with** `discriminatedUnion`:

```plaintext
// personDecoder.decodeAny(invalidData) produces:
Err: Unexpected discriminator value 'guest' for field 'type'. Expected one of: user, admin. Found in: {"type":"guest","id":"guest-001"}
```

(It tells you *exactly* the problem: the value `"guest"` wasn't expected for the `type` field).

If the `type` was correct but other data was wrong (e.g., `isActive` was a string for a `user`), `discriminatedUnion` would report the error *from within the* `userDecoder`, prefixed clearly:

```plaintext
// Example: { type: 'user', id: 'u1', name: 'Test', isActive: 'yes' }
Err: Error decoding variant with type='user': I expected to find a boolean but instead I found "yes":
occurred in a field named 'isActive'
```

### Get Started Today!

Decoding discriminated unions is now simpler, safer, and more efficient in `jsonous`. If you're working with tagged unions and JSON, the `discriminatedUnion` decoder is the tool you've been waiting for.

Update `jsonous` to the latest version and give it a try! We think you'll appreciate the improved ergonomics and clearer error reporting. Check out the README for detailed usage and examples.

Happy Decoding!
