TypeScript: es5.d.ts lib does not match the ECMAScript 5.1 Spec
Types in es5.d.ts Should Match the ECMAScript 5.1 Standard
As a TypeScript user, I expect that an environment configured with "lib": ["es5"] should only resolve types specified in the ECMAScript 5.1 Language Specifications. Any further type declarations necessary should be optionally imported or specified by the TypeScript user. Indeed, this appears to be the suggested usage, as pointed out by @mhegazy in #1995 (emphasis mine):
you can always augment your program with additional types that you know would exist on your target system. for instance, if you know you are always running on an engine that supports promises, just add declarations for Promise in your program. The idea is that the lib file reflects the baseline of engines supporting the target of the compilation (ES5 v.s. ES6)
However, including only the “es5” lib will also bring in the following non-ES5 types (and their support types):
- Decorators - Introduced when es5.d.ts was core.d.ts (2015/03/17).
PromiseLike- This was pulled into core.d.ts from es6.d.ts on 2015/05/05.ArrayBufferandTypedArrays - This was merged into core.d.ts from es6.d.ts and extension.d.ts on 2015/08/15.Promise<T>- This was pulled into es5.d.ts from es2015.promise.d.ts on 2016/02/13.- Several others not listed…
Including these types breaks expectations in properly initialized environments and has the potential to lead to confusion, if not bugs.
Why Are These Types In es5.d.ts to Begin With?
In short: to support the DOM API declarations.
From what I’ve been able to glean from the history, the fact that the above type declarations exist in es5.d.ts is effectively a kludge to more simply support the constantly-evolving DOM API libraries. From the comments on #14053 (which brought Promise<T> into es5.d.ts):
Move the declaration of the Promise interface to es5.d.ts (and subsequently lib.d.ts). This allows for DOM APIs that return Promise to be typed accurately.
Indeed, supporting DOM APIs appears to explain much of the reason for including the previously listed types in es5.d.ts. Again, in #16077 we have the following comment:
The sole purpose of including Promise and Typed Arrays to prevent errors from lib.dom.d.ts where APIs e.g. Fetch and Web Audio use them.
[It should be noted that in the above case, the discussion revolves around whether or not to move the types further back in time (to ES3).]
Moving “Support” Types Out of es5.d.ts
So the types are defined in the ES libs to support the DOM APIs (which suffer from granularity issues). Here are some options for how they might be removed from the es5.d.ts file:
1. Create a Middleman Library
It appears that ES5 is used as the default ECMAScript library for TypeScript - at some point es5.d.ts was even called core.d.ts. But a core.d.ts could still exist: it could import es5.d.ts and then some extra “support” types (e.g. those outlined above). That file could then be specified in the lib.d.ts library source map (as well as others).
2. Specify Necessary Features Directly in the Library Source Map
Specific features that DOM APIs require might be included from their respective “future” libraries. An example for promises might look like:
// ...
{ target: "lib.dom.d.ts", sources: ["header.d.ts", "dom.generated.d.ts"].concat("es2015.promise.d.ts") },
// ...
{ target: "lib.d.ts", sources: ["header.d.ts", "es5.d.ts"].concat(hostsLibrarySources, "es2015.promise.d.ts") },
// ...
provided Promise type interfaces were moved back into es2015.promise.d.ts (unless I’m missing something important).
3. Declare Necessary Interfaces Inline
Declare required, but possibly new/future types as empty interfaces inline in the libraries that require it. In at least one Node Type Declaration library I’ve encountered, they did just this:
interface MapConstructor { }
interface WeakMapConstructor { }
interface SetConstructor { }
interface WeakSetConstructor { }
What’s more, this even seems to be a suggested approach (emphasis mine):
you can always augment your program with additional types that you know would exist on your target system. for instance, if you know you are always running on an engine that supports promises, just add declarations for Promise in your program. The idea is that the lib file reflects the baseline of engines supporting the target of the compilation (ES5 v.s. ES6)
Wrap-up: Being Explicit About Type Definition
Instituting a change like this would not only clean up the ECMAScript type declaration file(s), it would explicitly document why those types were included with certain libraries. Presently, a user trying to figure out why UInt8Array is a suggested type when they’ve only imported type declarations for ECMAScript 5 ("lib": ["es5"]) will find that it’s confusingly included in the es5.d.ts file, sans any documentation for its mysterious inclusion.
Further, it would decouple DOM support from ECMAScript support, enabling users to more explicitly configure their environments without surprising side-effects.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 2
- Comments: 22 (5 by maintainers)
@SaschaNaz
This issue is not at all concerned with the default and explicitly states as much:
The DOM is included by default, yes, as it is extremely common to write frontend (browser) code. However, it is possible to customize the environment such that DOM APIs are removed (e.g.
"lib": ["es5"]). Node environments are just such a place (and old versions of Node, like some legacy browsers, did not have Promise support).The basic issue is that when I specify “ECMAScript 5” as my environment, I do not expect to see
Promises,TypedArrays, etc. In the case of #16077, pushing them into ES3 actually moves those types farther away from the spec in which they were actually introduced (ECMAScript 6).As for the default, one might argue based on the current ES6 API coverage in browsers that it should be updated to point to ECMAScript 6. But that is a different issue.
This is correct. The DOM doesn’t follow the same versioning scheme as ECMAScript, so we’re effectively trying to map an N-dimensional versioning space into a single dimension, which is definitely lossy.
My main concern here is that this is a cure that is potentially much worse than the disease. We still don’t actually bring in the values of the ‘future’ runtimes into scope, so you can’t really write expression code that tries to access a non-existent value. And arguably it’s not actually incorrect to use e.g. an ES6 type in an ES5 codebase – if you write
class Foo implements Promise {you haven’t actually done anything wrong, even in an ES3 codebase.This gets really confusing if you consider libraries that work perfectly in ES3/ES5 environments, but still understand ES6 constructs if given them. If a library knows how to call
.thenon aPromisewhen it sees one, but never tries to instantiate the Promise itself, is it wrong to sayfunction fn(x: Promise | string) {in the context of an ES3 program? Note that the solution here cannot be “writeinterface Promise { }” because now that function has no type safety whatsoever! So you tell ES3 users of this library that they have to includelib.promise.d.ts… which brings in the value, which is the very thing we should be most afraid of, and is already solved by the current solution? That’s a regression, not an improvement.