TIL: Explicit TypeScript lib References
October 08, 2020 • 4 minute read
The problem
When a package uses a modern JavaScript feature, its types can behave strangely if the consuming application’s TypeScript configuration isn’t up-to-date with the feature.
For example, if the package uses an ES2018
feature — like async iterables — but the application’s TypeScript configuration is ES2015
, the behaviour can be surprising and difficult to understand.
This has happened with RxJS. In version 7, an async iterable can be used as an observable input — it can be passed to an observable creator, like from
— so the ObservableInput
type looks like this:
export type ObservableInput<T> =
| SubscribableOrPromise<T>
| ArrayLike<T>
| Iterable<T>
| AsyncIterableIterator<T>;
If the TypeScript configuration for the application that consumes RxJS does not have lib
configured as ES2018
or later, the AsyncIterableIterator
type won’t be referenced and the type inference will behave in an unexpected manner.
Let’s take a look at this snippet:
const answers = from([42, 54]);
The observable input is an array of numbers, so the inferred type of answers
should be Observable<number>
, but it’s not. If lib
is configured as ES2017
or earlier, the inferred type is Observable<unknown>
.
And it’s not at all clear that the unexpected inference is related to the TypeScript configuration — in particular, to the lib
that’s specified. 😬
How does the configuration work?
There are two ways the TypeScript lib
compiler option can be configured.
-
No
lib
option is specified. In which case, TypeScript uses a thetarget
and adds references to theDOM
andScriptHost
libraries.For example, if the
target
isES5
, thelib
option will be["ES5", "DOM", "ScriptHost"]
. - The
lib
option can be specified explicitly. In which case, thetarget
option has no bearing on which libraries are referenced.
It’s common for the lib
option to be specified because:
- some developers (like me, before I wrote this post 😅) don’t read all of the TypeScript documentation and don’t understand how the
target
andlib
options interact; and - Node developers don’t want the
DOM
types to be referenced.
That means it’s quite likely that there will be some applications that won’t have TypeScript configured for modern features used within packages upon which they depend.
A solution
The problem can be avoided if the package is able to have some say in the TypeScript libraries that are referenced. Ryan Cavanaugh pointed out that this can be done by adding a TypeScript triple-slash directive to specify a lib
reference, like this:
/// <reference lib="ESNext.AsyncIterable" />
The directive can be added to a .ts
source file. When TypeScript compiles the source file, it will include the directive in the generated .d.ts
file. Then, when the consuming application imports the .d.ts
file, the directive will be processed and the specified library will be referenced.
The solution works. It resolves the problem mentioned above — with the directive, the type is correctly inferred to be Observable<number>
— but it’s not perfect. Ryan mentions that referencing ESNext.AsyncIterable
also references a handful of other types — some of which might not have been otherwise referenced by the application developer’s TypeScript configuration.
The solution is, however, much better than the alternative: countless developer hours wasted and an interminable number of issues opened. I mean, we will, of course, mention RxJS’s lib
requirement — a minimum of ES2018
— in the documentation, but who’s going to read that? Probably not me. 🙂