deno icon indicating copy to clipboard operation
deno copied to clipboard

Restore the ability to type-check stable code

Open jsejcksn opened this issue 5 months ago • 1 comments

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 ReferenceErrors 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 class, which is a global that's only available when using the --unstable flag since Deno v1.13.0:

example.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):

% deno check example.ts
Check file:///Users/deno/example.ts

Running the program crashes with an uncaught ReferenceError:

% 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 of the manual (📌):

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…

% 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:

% 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:

    /// <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:

    {
      "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.

jsejcksn avatar Jan 25 '24 18:01 jsejcksn