# typescript-forward-compatibility
Source: https://speakeasy.com/blog/typescript-forward-compatibility


Speakeasy TypeScript SDKs now support forward compatibility features that let your SDK gracefully handle API evolution. When you add a new enum value, extend a union type, or adjust response fields, SDK users on older versions continue working without errors.

This guide walks through how to enable these features in your existing SDK configuration.

## What's changing

New Speakeasy TypeScript SDKs now include forward compatibility features enabled by default. If you're upgrading an existing SDK, you'll need to opt in to these features in your `gen.yaml` configuration.

Here's a quick reference of the settings to add:

```yaml
typescript:
  forwardCompatibleEnumsByDefault: true
  forwardCompatibleUnionsByDefault: tagged-only
  laxMode: lax
  unionStrategy: populated-fields
```

Each of these settings addresses a specific class of API evolution issues. The sections below explain what each setting does and how to configure it.

1. [Forward-compatible enums](#forward-compatible-enums)
2. [Forward-compatible unions](#forward-compatible-unions)
3. [Lax mode](#lax-mode)
4. [Smart union deserialization](#smart-union-deserialization)

### Forward-compatible enums

Enums are one of the most common sources of breaking changes. When your API adds a new status, category, or type value, SDKs with strict enum validation will reject the entire response—even though the request succeeded.

To enable forward-compatible enums, add this to your `gen.yaml`:

```yaml
typescript:
  forwardCompatibleEnumsByDefault: true
```

**What this changes:** Any enum used in a response will automatically accept unknown values. Single-value enums won't be automatically opened. For finer control, use the `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` extension on individual enums in your OpenAPI spec.

**Example scenario:** Your notification system starts with `email` and `sms`, then you add `push` notifications. Without forward compatibility, SDK users on older versions see errors until they upgrade. With forward compatibility enabled, they receive the value gracefully:

```typescript
const notification = await sdk.notifications.get(id);
// Before: Error: Expected 'email' | 'sms' | 'push'
// After:  'email' | 'sms' | 'push' | Unrecognized<string>
```

When SDK users receive a known value, everything works exactly as before. Unknown values are captured in a type-safe `Unrecognized` wrapper.

### Forward-compatible unions

Similar to enums, discriminated unions often get extended as your API evolves. A linked account system might start with email and Google authentication, then you add Apple or GitHub options later.

To enable forward-compatible unions, add this to your `gen.yaml`:

```yaml
typescript:
  forwardCompatibleUnionsByDefault: tagged-only
```

**What this changes:** Tagged unions will automatically accept unknown discriminator values. For finer control, use `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` on specific unions in your OpenAPI spec.

**Example scenario:** You add GitHub as a new linked account type. SDK users on older versions won't crash—they'll receive an `UNKNOWN` variant with access to the raw response data:

```typescript
const account = await sdk.accounts.getLinkedAccount();
// Before: Error: Unable to deserialize into any union member
// After:
//   | { type: "email"; email: string }
//   | { type: "google"; googleId: string }
//   | { type: "UNKNOWN"; rawValue: unknown }  // Automatically inserted for open unions
```

### Lax mode

Sometimes the issue isn't new values being added, but missing or mistyped fields. A single missing required field can cause the entire response to fail deserialization, even when all the data SDK users actually need is present.

To enable lax mode, add this to your `gen.yaml`:

```yaml
typescript:
  laxMode: lax # or 'strict' to keep current behavior
```

**What this changes:** Lax mode applies sensible defaults when fields are missing or have minor type mismatches. It only kicks in when the payload doesn't quite match the schema—correctly documented APIs see no change in behavior.

**Why this matters:** The SDK types never lie to your users. Lax mode fills in sensible defaults rather than returning incorrect data, applying only non-lossy coercions. This approach is inspired by Go's built-in JSON unmarshal behavior and Pydantic's coercion tables.

For required fields that are missing, lax mode fills in zero values:

| Field Type       | Missing Value         | Default                 |
| ---------------- | --------------------- | ----------------------- |
| Required string  | `null` or `undefined` | `""`                    |
| Required number  | `null` or `undefined` | `0`                     |
| Required boolean | `null` or `undefined` | `false`                 |
| Required date    | `null` or `undefined` | `Date(0)` (Unix epoch)  |
| Required literal | `null` or `undefined` | The literal/const value |
| Required bigint  | `null` or `undefined` | `BigInt(0)`             |

For nullable and optional fields, lax mode handles the common confusion between `null` and `undefined`:

- Nullable field that received `undefined` is coerced to `null`
- Optional field that received `null` is coerced to `undefined`

Lax mode also provides fallback coercion for type mismatches:

- Required string: any value coerced to string with `JSON.stringify()`
- Required boolean: coerces the strings `"true"` and `"false"`
- Required number: attempts to coerce strings to valid numbers
- Required date: attempts to coerce strings to dates, coerces numbers (in milliseconds) to dates
- Required bigint: attempts to coerce strings to bigints

### Smart union deserialization

When deserializing union types, the order of options matters. The default `left-to-right` strategy tries each type in order and returns the first valid match. This works well when types have distinct required fields, but can pick the wrong option when one type is a subset of another.

To enable smart union deserialization, add this to your `gen.yaml`:

```yaml
typescript:
  unionStrategy: populated-fields
```

**What this changes:** Instead of picking the first valid match, the SDK tries all union options and returns the one with the most matching fields (including optional fields). This approach is inspired by Pydantic's union deserialization algorithm.

**How it works:** The algorithm attempts to deserialize into all union options and rejects any that fail validation. It picks the candidate with the most populated fields. If there's a tie, it picks the candidate with the fewest "inexact" fields (where coercion happened, such as an open enum accepting an unknown value).

**Example scenario:** You have a union of `BasicUser` and `AdminUser` where `AdminUser` has all the fields of `BasicUser` plus additional admin-specific fields. The `populated-fields` strategy correctly identifies admin users even if `BasicUser` is listed first in the union.

## Complete upgrade checklist

To upgrade your existing TypeScript SDK with all forward compatibility features, add these settings to your `gen.yaml`:

```yaml
typescript:
  forwardCompatibleEnumsByDefault: true
  forwardCompatibleUnionsByDefault: tagged-only
  laxMode: lax
  unionStrategy: populated-fields
```

After updating your configuration, regenerate your SDK with `speakeasy run`. The changes are backward compatible—your SDK users won't need to change their code, but they'll benefit from improved resilience to API evolution.

## What to expect

These features work together to provide a robust experience for your SDK users:

- **Forward-compatible enums and unions** handle additive changes to your API
- **Lax mode** handles missing or mistyped fields gracefully
- **Smart union deserialization** handles ambiguous type discrimination

All without sacrificing the type safety and developer experience that make TypeScript SDKs valuable.

If you run into any issues during the upgrade or have questions about specific scenarios, reach out to the Speakeasy team.

_Note: This post focuses on TypeScript SDK behavior. Speakeasy SDKs for other languages (Python, Go, Java, and more) implement similar forward compatibility behaviors tailored to each language's idioms._