Extension types in TypeScript
Tags: javascript, typescript, functional
Published: 2020-08-06 16:15:15

---
title: Extension types in TypeScript
description: How to work with intersection and union extensions.
canonic_url: https://meeshkan.com/blog/extension-types-ts/
published: true
tags:
  - javascript
  - typescript
  - functional
---

When creating a TypeScript library, you will invariably run into a scenario where someone wants to extend it. In JavaScript land, they can do this with the following hack:

```javascript
const yourLibrary = require('your-library');
const myExtension = require('./my-extension');
yourLibrary.yourObject.myExtension = myExtension
```

In TypeScript, this type of thing is generally frowned upon because the type system will not (easily) allow monkey patching. The “classic” workaround is using a cast to the `any` type, which effectively de-TypeScript-ifies TypeScript.

```typescript
import { yourObject } from 'your-library';
import myExtension from 'my-extension';
(<any>yourObject).myExtension = myExtension
```

The other problem with this pattern is that it `myExtension` is not referenceable on `yourObject`, which requires a cast on every access as well.

```typescript
const result = (<any>yourObject).myExtension.myFunction();
```

At this point, we have lost the type-safety of `myExtension`. So the TypeScript compiler will no longer check that `myFunction` actually exists on `myExtension`, let alone what the result of `myFunction()` is. Too much of this pattern will render your TypeScript project un-typeable, at which point JavaScript would have been a better option.

## What to do?

One solution is just to make a pull request to the original library to have your extension incorporated into its official package. While this may be a good idea in some scenarios, in most cases extensions are too niche, too broken or too big to be merged into a project. On top of that, pull requests often take a long time to go through review and incorporation into a new release.
Another solution, which is what this article advocates, is building libraries with type-safe extensions as first-class citizens. I realize that this won’t solve your problem here-and-now, but if you are a library author, it will give other developers an easy way to extend your work without necessarily touching the core package. If you are a consumer of a library, a pull-request giving it extensional properties is usually a much easier ask than extending the library with _your_ specific feature.

## Two types of extensions

The most common types of extensions developers need are _intersection extensions_ and _union extensions_. Intersection extensions say “Hey, your objects are awesome, but they would be even _more_ awesome if they did thing X.” Union extensions say “Hey, your objects are awesome, but you’re missing a few that I’d need for scenario Y.” Intersections and unions are part of TypeScript’s core language — the intersection `&` and union `|` operator are a basic way to build composite types. What I’m advocating is leveraging these operators to supercharge your libraries with extensionality.

### Intersection extensions

Intersection extensions can be achieved with a generic type (let’s call it `U`) that is passed down through your objects and intersected with primitive objects via the `&` operator.

Let’s imagine that your library contains the following two types.

```typescript
type Person = {
  name: string;
  address?: Address;
  friends?: Person[];
}
type Address = {
  city: string;
  country: string;
}
```

Intersection extensions add an intersection to all relevant types.

```typescript
type Person<U> = {
  name: string;
  address?: Address<U>;
  friends?: Person<U>[];
} & U;
type Address<U> = {
  city: string;
  country: string;
} & U;
```

For example, if we want to add an optional `id` to all types, it becomes a simple operation.

```typescript
const me: Person<{id?: number}> = {
  name: 'Mike',
  address: {
    id: 5,
    city: 'Helsinki',
    country: 'Finland'
  },
  friends: [{ name: 'Marie', id: 101 }]
}
```

Even better, we now have a type-safe accessor for `id`, so the following function will pass the TypeScript compiler

```typescript
const hasId = (p: Person<{id?: number}>) => typeof p.id === 'number';
```

### Union extensions

Let’s imagine a different scenario — we are creating types for JSON objects.

```typescript
type JSONPrimitive = number | boolean | string | null;
type JSONValue = JSONPrimitive | JSONArray | JSONObject;
type JSONObject = { [k: string]: JSONValue; };
interface JSONArray extends Array<JSONValue> {}
```

Let’s say that we would like JavaScript `Date` objects to also be admitted as JSON. Union extensions, which I’ll represent with the letter `T`, give us a clean way to do this.

```typescript
type JSONPrimitive<T> = number | boolean | string | null | T;
type JSONValue<T> = JSONPrimitive<T> | JSONArray<T> | JSONObject<T>;
type JSONObject<T> = { [k: string]: JSONValue<T>; };
interface JSONArray<T> extends Array<JSONValue<T>> {}
```

Now, we can put `Date` objects all over our JSON and the TypeScript compiler will not complain.

```typescript
const jsonWithDates: JSONValue<Date> = {
  foo: 1,
  bar: new Date(),
  baz: [true, 'hello', 42, new Date()]
}
```

### Runtime validation

If you are using a runtime type validator like [`io-ts`](https://github.com/gcanti/io-ts), the patterns are quite similar. For intersections, we can use the `intersection` function from `io-ts`.

```typescript
import * as t from 'io-ts';
const PersonValidator = <U>(u: t.TypeOf<U, U>) = t.recursion(
  'Person',
  t.intersection([
    t.type({name: t.string}),
    t.partial({
      address: AddressValidator(u),
      friends: t.array(PersonValidator(u))
    }),
    u
]));
const AddressValidator = <U>(u: t.TypeOf<U, U>) = 
  t.intersection([
    t.type({city: t.string, country: t.string}),
    u    
  ]);
```

The same type of pattern can be used for union types — just pass the a validator to `t.union` instead of `t.intersection` where needed.

## Show me the code!

This is the strategy I used to build [`json-schema-strictly-typed`](https://github.com/meeshkan/json-schema-strictly-typed), which creates a typed version of JSON Schema that is extensible with both intersection and union extensions. This way, people can add arbitrary extensions to objects in the Schema (intersection) _and_ arbitrary new Schema primitives (union).

From this level of generic-ness (genericity?, generickitude?), it is easy to export helper objects for “base” cases. The base case of an intersection extension is simply a type from which all your objects already extend. In the example above, `Person<{}>` and `Address<{}>` would be this, as intersecting with `{}` is a no-op. For union types, a default extension could be extending the library with a type that already exists in the union. So, for example, `JSONSchemaValue<string>` is a no-op.

I’m looking forward to seeing if this pattern catches on and if the community can come up with tools that help maintain and author TypeScript libraries with extensibility in mind!