Repository: deno
Issue: Restore the ability to type-check stable code
Labels: suggestion,types

Ref:
- https://github.com/denoland/deno/pull/21825

Here's the message accompanying the linked PR for context here:

> This commit removes conditional type-checking of unstable APIs.
> 
> Before this commit `deno check` (or any other type-checking command and the LSP) would error out if there was an unstable API in the code, but not `--unstable` flag provided.
> 
> This situation hinders DX and makes it harder to configure Deno. Failing during runtime unless `--unstable` flag is provided is enough in this case.

I think the change might have been introduced prematurely, as there is no longer a simple way to type-check stable code.

The change completely mitigates one of the useful safety features of type-checking: preventing use of references that won't exist at runtime for stable code (code that does not use the `--unstable` runtime argument). By including all of the unstable type declarations during type-checking for all programs unconditionally, an entire class of preventable runtime [`ReferenceError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError)s are no longer caught. Although I don't have usage data to support it, I expect that the overwhelming majority of Deno users are not running code using the `--unstable` flag, _especially in production_.

Here's a contrived example for demonstration which uses the [`WebSocketStream`](https://deno.land/api@v1.40.1?unstable=&s=WebSocketStream) class, which is a global that's only available when using the `--unstable` flag since [Deno v1.13.0](https://github.com/denoland/deno/releases/tag/v1.13.0):

`example.ts`:
```ts
const wss = new WebSocketStream("ws://localhost:8000/live");

const { readable } = await wss.opened;

for await (const _data of readable) {
  // code that handles the socket stream…
}

```

Type-checking passes (no diagnostics):

```txt
% deno check example.ts
Check file:///Users/deno/example.ts
```

Running the program crashes with an uncaught `ReferenceError`:

```txt
% deno run example.ts
error: Uncaught (in promise) ReferenceError: WebSocketStream is not defined
const wss = new WebSocketStream("ws://localhost:8000/live");
            ^
    at file:///Users/deno/example.ts:1:13
```

The problem is exaggerated when type-checking is run prior to execution using the `--check` argument (`deno run --check …`). The expectation is that a type-checking failure will occur for these kinds of reference errors and prevent execution of the code entirely. From the [TypeScript section](https://docs.deno.com/runtime/manual/advanced/typescript/overview#type-checking) of the manual ([📌](https://github.com/denoland/deno-docs/blob/fd187b08972c9b053340242b836d57a8f387ce52/runtime/manual/advanced/typescript/overview.md?plain=1#L85C65-L88C27)):

> When using `deno run` _with_ the `--check` argument, type-related diagnostics will prevent the program from running: it will halt on these warnings, and exit the process before executing the code.

This was true in previous releases…

```txt
% deno --version
deno 1.39.4 (release, aarch64-apple-darwin)
v8 12.0.267.8
typescript 5.3.3

% deno run --check example.ts
Check file:///Users/deno/example.ts
error: TS2304 [ERROR]: Cannot find name 'WebSocketStream'.
const wss = new WebSocketStream("ws://localhost:8000/live");
                ~~~~~~~~~~~~~~~
    at file:///Users/deno/example.ts:1:17
```

…but after the change, these unavailable reference errors are not caught prior to runtime, and instead they create exceptions:

```txt
% deno --version
deno 1.40.1 (release, aarch64-apple-darwin)
v8 12.1.285.6
typescript 5.3.3

% deno run --check example.ts
Check file:///Users/deno/example.ts
error: Uncaught (in promise) ReferenceError: WebSocketStream is not defined
const wss = new WebSocketStream("ws://localhost:8000/live");
            ^
    at file:///Users/deno/example.ts:1:13
```

The error occurs almost instantly in the simple, contrived example — but in other typically-complex code, a reference error might not be encountered until a deep, nested condition is met somewhere long after execution starts.

---

Workarounds are available at present, but tedious — they require modifying _every_ program which doesn't use unstable features in order to emulate the previous default compiler behavior. Options include:

- adding triple-slash directives to every entrypoint module:

   `example.ts`:
   ```ts
   /// <reference no-default-lib="true" />
   /// <reference lib="deno.window" />
   
   const wss = new WebSocketStream("ws://localhost:8000/live");
   
   const { readable } = await wss.opened;
   
   for await (const _data of readable) {
     // code that handles the socket stream…
   }
   
   ```

- explicitly configuring compiler options in the program's associated config file:

   `deno.json`:
   ```json
   {
     "compilerOptions": {
       "lib": ["deno.window"]
     }
   }
   ```

---

I'm sympathetic to users who want an LSP-enabled experience with less friction while using unstable features… but this change seems like a clear regression, and I hope we can find a better solution.