Mutually exclusive types done right
Tags: javascript, typescript, programming
Published: 2022-04-13 10:24:43

The difference between `type`s and `interface`s in Typescript is not necessarily clear since they look very similar. However, there are some subtle differences and in this article I will focus on a feature that can be achieved only with types : describing mutually exclusive types.

Let's start with an exemple :

```typescript
type Resource<Type = any> = {
    progress: 'pending' | 'success' | 'error'
    error?: Error
    data?: Type
}
```

Such structure would be used as follow :
- set `progress` to `"pending"` when accessing the resource
- then set `progress` to `"success"` and the data to `data` in case of success
- or set `progress` to `"error"` and the error to `error` in case of failure

However, this is not enforced in any way ; let's try to write some hardcoded values :

```typescript
// this one is a possible expected value :
const res: Resource = {
    progress: 'pending'
}
```

```typescript
// but this one is unexpected, however I can write it !
const res: Resource = {
    progress: 'pending',
    data: 42             // 😵 oh no ! this is allowed
}
```

```typescript
// this one doesn't make sense either !
const res: Resource = {
    progress: 'pending',
    data: 42,            // 😵
    error: new Error()   // 😵
}
```

# Union type to the rescue

One strength of types in Typescript is to have the possibility to combine types together. Let's rewrite the type as the union of 3 mutual exclusive types :

```typescript
type Resource<Type = any> = {
    progress: 'pending'
} | {
    progress: 'success'
    data: Type
} | {
    progress: 'error'
    error: Error
}
```

And it works fine ! Now if I write :

```typescript
// I can't write it anymore ! There is an error !
const res: Resource = {
    progress: 'pending',
    data: 42             // 👈 yes ! there is an error
}
```

```typescript
// the fix :
const res: Resource = {
    progress: 'success', // 👌
    data: 42
}
```

So far so good, Typescript now prevent mistakes the way expected. And it also prevent writing the following code, because `data` might not be a field of `res` :

```typescript
const res: Resource = getSomeResource();
doSomething(res.data); // 👈 error !

```

To access the data, we must use type guards which allow Typescript to refine the real type in an alternative branch of code :

```typescript
const res: Resource = getSomeResource();
if (res.progress === 'success') {
    doSomething(res.data); // 👌
}
```

Type guards are certainly one of the most valuable features in Typescript, but unfortunately, they prevent also writing this :

```typescript
const res: Resource = getSomeResource();
const data = res.data ?? 42; // 😖 oh no ! error !
```

Why ? Let's say it again: `data` might not be a field of `res` !

# Mutually exclusive type done right

So let's fix our type in order to have the `data` field defined in any case :

```typescript
type Resource<Type = any> = {
    progress: 'pending'
    data?: never
    error?: never
} | {
    progress: 'success'
    data: Type
    error?: never
} | {
    progress: 'error'
    data?: never
    error: Error
}
```

That way, the `data` field is an unconditionally part of the result union type ; what changes is that sometimes we can't set it :

```typescript
// I still can't write that !
const res: Resource = {
    progress: 'pending',
    data: 42             // 👈 yes ! there is an error !
}
```

...but all the times we can access it :

```typescript
// but now, I can write that :
const res: Resource = getSomeResource();
const data = res.data ?? 42; // 👌 got it !
```

...and of course type guards are still working the expected way !

As a result, the last type definition is a bit verbose, but the code will be less ! Therefore it will be worth to apply this technique only in certain circumstances.

---

Thank you for reading, Typescript Padawan !