Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

JS type and name resolution #36

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
265 changes: 265 additions & 0 deletions reference/JSDoc-Type-References.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
In Javascript files, the compiler understands Typescript types in
JSDoc comments like `@type` and `@param`. However, it has additional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a link to the page you pointed me to? (With a TODO to switch to the new page when it's in this repo?)

rules to make type resolution work better for Javascript code that
wasn't written with machine checking in mind. These rules apply only
in JSDoc in Javascript files, and some can be disabled by setting
`"noImplicitAny": true`, which the compiler takes as a signal that the
code is being written with Typescript in mind.

This document first describes the way Javascript type resolution is
different, because those differences are bigger and more surprising.
Then it covers differences in name resolution.

## Simple Rewrites ##

The simplest Javascript-only rule is to rewrite the primitive object
types to the usual primitive types, plus `Object` to `any`. These are
commonly used interchangeably in JSDoc:

Type | Resolved Type
------------|--------------
`Number` | `number`
`String` | `string`
`Boolean` | `boolean`
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to know, but very confusing... I'm guessing that in jsdoc either they're the same or people don't make a distinction, but then I saw that TS does have Number etc, and I can't figure out the difference or why number is preferable. (Maybe this is the answer, and if so add a link to it, or better, to the thing it points to.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather link to another reference page that explains the difference. I didn't use the correct terms here, which probably made googling harder.

`Object` | `any`

In Typescript, the primitive type `number` is nearly always the right
type; whenever methods from the object type `Number` are needed, the
compiler asks for the apparent type anyway, and the apparent type of
`number` is `Number`.

Neither `object` nor `Object` have defined properties or string index
signatures, which means that they give too many errors to be useful in
Javascript. Until Typescript 3.7, both were rewritten to `any`, but we
found that few people used `object` in JSDoc, and that more and more
JSDoc authors wanted to use `object` with its Typescript meaning. The
rewrite `Object -> any` is disabled when `"noImplicitAny": true`,
which the compiler takes as a signal that the code is being written
with Typescript in mind.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe drop "which ... mind" since it's mentioned at the top?


There are also rewrites of other JSDoc types that are equivalent to
built-in Typescript types:

Type | Resolved Type
----------------------|--------------
`Null` | `null`
`Undefined` | `undefined`
`Void` | `void`
`function` | `Function`
Comment on lines +45 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yet more confusion, especially since this is unlikely to have the same explanation as the above--?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will write more explanations for all of these.

`Object<string,Type>` | `{ [x: string]: Type }`
`Object<number,Type>` | `{ [x: number]: Type }`

The first three of this list are commonly used in unchecked
Javascript, while the last three are commonly used in Javascript that
is checked by the Closure compiler.

Finally, `array` and `promise` rewrite to `Array<any>` and `Promise<any>`
respectively, since they, too, are used without regard to case:

Type | Resolved Type
------------|--------------
`array` | `any[]`
`promise` | `Promise<any>`

Note that `array<number>` does *not* rewrite to `number[]`; it's just
an error. Same for `promise<number>`. On the flip side, `Array` and
`Promise` result in `Array<any>` and `Promise<any>` because of another
rule that defaults missing type arguments to `any` in Javascript.

### Where To Find The Code ###

`getIntendedJSDocTypeReference` in `checker.ts`.

## Values as Types ##
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be honest, I'd prefer to never mention this in any documentation ever so that we don't imply that we think relying on it is a good idea (it's not).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like maybe we could talk about it in the TypeScript necronomicon or something, but I'd leave it out of the handbook.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reference pages are the necronomicon, or the closest we'll get in an open-source team.

This is why I've never written it down before, though. I hoped it would "just work" for most people.

Copy link
Member

@weswigham weswigham Dec 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, like, rust has a hard split between "The Rust Book" and "The Rustonomicon" (which is what I was referencing before). While documenting the current state of esoteric edge cases in, say, conditional type resolution or js type lookup is laudable, I don't think it should be adjacent to information that's much more mainstream - it serves mostly to distract. Like, I think the "what simple remappings do we do (String -> string)" is more useful for casual reading and documentation, while "what fallback code exists within the compiler, approximately" is heavyweight "I wanna know how the compiler internals tick" stuff. This article does a bit of both - I'd split it into two pieces, down those viewership lines, with one targeted at each audience, tbh.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're going for the same two categories, but with a softer split. Look at the other documents in reference/ -- they cover the same kinds of things.

I think it is worthwhile to put the first section's information into the handbook under checkJs though.
It's just that that chapter doesn't exist yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that Compiler Options belongs elsewhere? Because that is just a reference, while the rest are really technical internal discussions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably. Not sure where it should go, though. @RyanCavanaugh opinions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think this is good case for splitting into two articles - my goal when I get my hands on doing new handbook stuff is to try split the existing doc into three main camps:

  • Language Learners
  • Experts looking to debug why something isn't what they expect
  • People wanting to faff with tooling

And each doc on any topic should ideally only be trying to address one of those three people (e.g. don't try mix the descriptions of config settings deeply in with a language feature)

I expected we'd need a bunch of docs for JSDoc though, so I wouldn't worry too much now about how it splits because once it starts becoming public facing we'll have more info

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orta when you say "faff with tooling", are you thinking of API consumers? This article is mostly people interested in compiler internals, with a touch of "experts' debugging help". That's what I was thinking of for the Assignability page too; realistically knowing the mechanics of assignability isn't going to help debugging much.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example "TypeScript with webpack" or "TypeScript with eslint" user-level stuff - not this depth 👍


The compiler maintains three namespaces: one each for values, types and
namespaces. Put another way, each name can have a value meaning, a
type meaning and a namespace meaning. However, there are fewer ways
to declare and reference types in Javascript, and the distinction
between type and value can be subtle, so for Javascript, the compiler
tries to resolve type references as values if it can't find a type.

Here's an example:

```ts
const FOO = "foo";
const BAR = "bar";

/** @param {FOO | BAR} type */
function f(type) {
}
```

In the compiler, this is resolved in two stages. Both stages use a
JSDoc fallback. First, in `getTypeFromTypeReference`, after first
checking the simple rewrites above, the compiler
resolves the name `FOO`. Here's a simplified version of the name resolution code:

```ts
let symbol: Symbol | undefined;
let type: Type | undefined;
type = getIntendedTypeFromJSDocTypeReference(node);
if (!type) {
symbol = resolveTypeReferenceName(node, SymbolFlags.Type);
if (symbol === unknownSymbol) {
symbol = resolveTypeReferenceName(node, SymbolFlags.Value);
}
type = getTypeReferenceType(node, symbol);
}
```

Because `resolveTypeReferenceName` fails to find a type named `FOO`,
the code makes a second calls looking for a value named `FOO`, which
is found. Then it passes the `FOO` symbol to
`getTypeReferenceType`, whose code looks a bit like this:

``` ts
if (symbol === unknownSymbol) {
return errorType;
}
const t = getDeclaredTypeOfSymbol(symbol);
if (t) {
return t;
}
if (symbol.flags & SymbolFlags.Value && isJSDocTypeReference(node)) {
return getTypeOfSymbol(symbol);
}
return errorType;
```

In the same way as name resolution, type resolution first looks for
the type of the type declaration of a symbol with `getDeclaredTypeOfSymbol`.
But there *is* no type declaration for `FOO`, so it returns
`undefined`. Then the fallback code looks for the type of the value
declaration for `FOO`, which it finds to be `"foo"`.

For comparison, here is an example that has both a value declaration
and a type declaration:

```ts
var i = 0;
interface i {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor: aren't ;s preferred?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uggh yes, just not by me personally. I'll go and add them in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

(I happen to love that they are, as well as "s...)

e: 1;
m: 1;
}
```

If you ask for the type of the value declaration of `i` &mdash; say, with
`/** @type {typeof i} */` &mdash; you'll get `number` from `var i = 0`. If
you ask for the type of `i`'s type declaration (with `/** @type {i} */`
this time), you'll get `{ e: 1, m: 1 }` from the `interface`
declaration.

But in Javascript, you can't even write `interface`, so what should
`/** @type {i} */` mean if your program is just:

```js
var i = 0
```

With no type declaration, the compiler just reuses the type of the value
declaration: `number`. This is equivalent to inserting the `typeof`
type operator to get `typeof i`. The effect is to make types more
Javascripty, allowing you to specify objects as example types without
having to learn about the difference between type and value:

``` js
/**
* @param {number} n
* @param {number} m
* @returns {-1 | 0 | 1}
*/
function exampleCompare(n, m) {
}

/** @param {exampleCompare} f */
function sort(f, l) {
// ...
}
```

This also works with objects, whether anonymous or an instance of a class:

``` js
const initial = {
frabjous: true,
beamish: true,
callooh: "callay",
Comment on lines +185 to +187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Made me spill some time on google...)

}

/** @param {initial} options */
function setup(options) {
}
```

Relying on this fallback for complex types does produce a simpler
type, but makes the equivalent Typescript really confusing to produce:

``` js
import { options } from './initial'
/**
* @param {keyof options} k
* @param {options[keyof options]} v
*/
function demo(k, v) {
options[k] = v;
}
```

is equivalent to the Typescript:

```ts
import { options } from './initial'
function demo(k: keyof typeof options, v: typeof options[keyof typeof options]) {
}
```

Knowing where to insert the missing `typeof` operators requires more
experience with complex type than most people can be expected to have.

### Where To Find The Code ###

In `checker.ts`, `getTypeFromTypeReference` contains the symbol
resolution fallback. Then it calls `getTypeReferenceType`, which
contains the type resolution fallback.

## Bonus: `@enum` tag ##

The `@enum` tag is quite unlike Typescript's `enum`. Instead, it's
basically a `@typedef` with some additional checking. Here's an
example:


``` js
/** @enum {string} */
const ProblemSleuth = {
mode: "hard-boiled",
compensation: "adequate",
};
```

The name of the type comes from the subsequent declaration
`ProblemSleuth`, which also works for `@typedef`. In fact, the
meaning is almost equivalent to a `@typedef` followed by a
`@type`:

``` js
/** @typedef {string} ProblemSleuth */
/** @type {{ [s: string]: ProblemSleuth }} */
const ProblemSleuth = {
mode: "hard-boiled",
compensation: "adequate",
};
```

The difference is that, although `ProblemSleuth`'s initializer is
checked for assignability to the index signature type, the type of the
value `const ProblemSleuth` is not the index signature type. It's
still the type of the initializer. It has exactly two properties, both
of type `string`. Because of that type, assignment of new properties
is not allowed, so the following is an error:

``` js
ProblemSleuth.solicitations = "numerous"
```