Advanced Typescript: dynamic return types without using generics
Tags: typescript, library
Published: 2019-10-15 09:21:29

*This post is aimed at library authors*

While creating the [rSchedule](https://gitlab.com/john.carroll.p/rschedule) recurring date library, I ran into a problem. The library facilitates processing recurring events (e.g. "every Tuesday starting on 2019/10/10") and I created it to be date-library agnostic. Depending on what date library you are using, rSchedule's objects should return dates using the user's chosen date library. But how to accomplish this while maintaining proper typing?

For example, if you use the `MomentDateAdapter` with rSchedule, rSchedule objects should return `Moment` dates. The obvious way to do this is via generics. This quickly becomes annoying though, as *every* rSchedule type in a given application will be providing the same generic argument, over an over again. Want to create a new recurrence `Rule`? You'll need to do so via `Rule<MomentDateAdapter>`, etc.  I wondered if there was a better way...

A much better approach would be if typescript realized you imported a particular date adapter, and then updated all objects to return the appropriate dates. Well, it turns out you *can* do this.

For example, if you use rSchedule after importing the `MomentDateAdapter` (once), then `rule.occurrences()` is typed as returning `MomentDateAdapter` objects for the entire application. However, if you were to use rSchedule after importing the `LuxonDateAdapter`, then `rule.occurrences()` is typed as returning luxon `DateTime` objects.

```ts
import '@rschedule/moment-date-adapter/setup'
import { Rule } from '@rschedule/core/generators';

const rule = new Rule();

const dates = rule.occurrences().toArray().map(({date}) => date);

// dates is typed as `Moment[]`
```
vs

```ts
import '@rschedule/luxon-date-adapter/setup'
import { Rule } from '@rschedule/core/generators';

const rule = new Rule();

const dates = rule.occurrences().toArray().map(({date}) => date);

// dates is typed as `DateTime[]`
```

The trick comes from the ability to turn a typescript union into a typescript intersection, combined with typescript [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html).

For example, given the following interface:

```ts
interface MyCustomDateTypeInterface {
  std: object;
  moment: Moment;
}
```

We can produce the type `object & Moment` via 

```ts
// taken from https://stackoverflow.com/a/50375286/5490505
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void)
    ? I
    : never;

type MyCustomDateType = UnionToIntersection<MyCustomDateTypeInterface[keyof MyCustomDateTypeInterface]> // equals `object & Moment`
```

From here, we can update `MyCustomDateType` using declaration merging to add `key: value` entries to `MyCustomDateTypeInterface`.

For example, if `MyCustomDateTypeInterface` started like this:

```ts
interface MyCustomDateTypeInterface {
  std: object;
}
```

We could update it to include `moment: Moment` by declaring 

```ts
interface MyCustomDateTypeInterface {
  moment: Moment;
}
```

At that point, `MyCustomDateType` would update from being `object` to `object & Moment`! This is the black magic rSchedule uses to achieve generic status without using generics!