Advanced Typescript: dynamic return types without using generics
John Carroll
Posted on October 15, 2019
This post is aimed at library authors
While creating the 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.
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
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.
For example, given the following interface:
interface MyCustomDateTypeInterface {
std: object;
moment: Moment;
}
We can produce the type object & Moment
via
// 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:
interface MyCustomDateTypeInterface {
std: object;
}
We could update it to include moment: Moment
by declaring
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!
Posted on October 15, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 15, 2024