Skip to content

Proposal: optionally declared variables #23602

Closed

Description

Scenario

When writing code that will run in both Node.js and browser environments (e.g. a React component compatible with SSR), it's sometimes necessary that part of the code only executes in one environment and not the other.

A few browser-only examples:

  • document.querySelector() to find an element in the DOM.
  • document.title = "new title" to update the page title.

To avoid executing these in Node.js, the following pattern is used:

if (typeof document !== "undefined") {
  // Safely use `document`.
  document.title = "new title";
}

Now the developer needs to configure TypeScript to include the types for document. It basically boils down to them deciding whether or not to include dom in the lib compiler option.

(dom.d.ts is distributed with TypeScript and includes:)

declare var document: Document;

Unfortunately both options have compromises:

  • don't include it and document won't be declared, and TypeScript will warn Cannot find name 'document'.
  • do include it and document will be declared as a Document, but not as Document | undefined, so strict-type-predicates will complain that the check is unnecessary

A motivated developer may choose to fork dom.d.ts and add | undefined to all of the global variable declarations, and satisfy strict-type-predicates.

However one final piece of safety is still missing: ensuring typeof document !== "undefined" is used, rather than document !== undefined. At runtime if document (or any name) isn't declared, and is used outside typeof, a ReferenceError is thrown. For this reason it's necessary to always choose typeof when performing environment "sniffing".

TypeScript currently cannot model "optionally declared variables", where a variable "might" be declared. Without this, TypeScript doesn't have enough information to warn about code that may throw ReferenceError.

Proposal

Introduce new syntax and type system concepts to allow the "optional declaration" of variables. Emit would be unaffected.

Proposed syntax:

declare? var window: Window;

This is distinct from:

declare var window: Window | undefined;

In the first case, it's describing that window may not be declared, and referencing window may throw a ReferenceError. In the second case, window is always declared, but might have the value undefined.

Implementation:

  • The grammar would need to be extended to match ? following declare.
  • The AST would need a representation (e.g. a flag for DeclareKeyword or a new OptionalDeclareKeyword).
  • The type system would need to track if a variable is optionally-declared, and support type-narrowing to a standard variable via typeof guards.
  • For this to actually be useful, the type checker should warn if "optionally declared" variables are referenced.
  • If a variable is "optionally declared", that information should be included in the string representation (e.g. declare? var document: Document), e.g. when hovering a name in VS Code.

Shortcomings:

  • It would be convenient if a single typeof window !== "undefined" guard could safely grant access to other browser globals (e.g. document, performance, etc). This current proposal would require separate typeof checks for each global.

Activity

  1. mhegazy commented on Apr 21, 2018

    @mhegazy
    Contributor

    So the expectation here is that lib.d.ts would have window declared conditionally; this means that every app written for web has to check for the existence of window. I do not think this is reasonable.

    that said, the proposal itself has merit. I would just say the ? should be after the var name and not the declare modifier.

  2. estaub commented on Apr 22, 2018

    @estaub

    Mohamed Hegazy (@mhegazy) I'm not sure, but I believe the semantics need to be different from e.g.window?. In the proposal's case, the variable may not exist as a reference at all. In the window? case, the variable is guaranteed to exist as a reference (whose value may be undefined). Of course, the guarantee is false (sometimes) when used with window, hence the proposal for a language element with different semantics.

    Having said that... while I think the proposal is good within its scope, I don't think it's very important, relative to https://github.com/Microsoft/TypeScript/issues.

  3. bradleyayers commented on Apr 23, 2018

    @bradleyayers
    Author

    So the expectation here is that lib.d.ts would have window declared conditionally; this means that every app written for web has to check for the existence of window. I do not think this is reasonable.

    I agree. I would expect this proposal would be used to create a derivative versions of dom.d.ts or @types/node declaration files that could be used by projects with these target environments.

    The derivative versions could be created by a script that rewrites .d.ts files to use optional declarations.

    that said, the proposal itself has merit. I would just say the ? should be after the var name and not the declare modifier.

    Putting it after the name would be more consistent with existing syntax, so I can see the benefit of that. I'm curious if the semantic differences with optional interface fields would be undesirable. declare var window?: Window would mean either:

    • not declared, or
    • declared and of type Window (i.e. never undefined)

    Compared to an optional interface field bar in `interface IFoo { bar?: Bar } which is:

    • not declared, or
    • declared and of type undefined, or
    • declared and of type Bar

    Perhaps because since a ReferenceError would never be thrown in the interface scenario, the difference is inconsequential.

  4. estaub commented on Apr 23, 2018

    @estaub

    Consider the difference in meaning for optional function parameters as well.

  5. mhegazy commented on Apr 23, 2018

    @mhegazy
    Contributor

    Possibly another use case of missing from #13195

  6. ganeshkbhat commented on Jul 11, 2018

    @ganeshkbhat

    You could have or add something like this as well:

    declare? var Worker: Worker || var PseudoWorker: any;

    Reference:
    #25570

  7. bradleyayers commented on Feb 3, 2019

    @bradleyayers
    Author

    I believe I'm going to be satisfied with #29332 and a lint rule to ban global access for things like window (so that both typeof window and window !== undefined would be banned). I expect instead I'll write code as:

    const { window } = globalThis;
    if (window) {
      // do something with window
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions