Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: optionally declared variables #23602

Closed
bradleyayers opened this issue Apr 21, 2018 · 7 comments
Closed

Proposal: optionally declared variables #23602

bradleyayers opened this issue Apr 21, 2018 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@bradleyayers
Copy link

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.
@mhegazy
Copy link
Contributor

mhegazy commented Apr 21, 2018

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.

@mhegazy mhegazy added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Apr 21, 2018
@estaub
Copy link

estaub commented Apr 22, 2018

@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.

@bradleyayers
Copy link
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.

@estaub
Copy link

estaub commented Apr 23, 2018

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

@mhegazy
Copy link
Contributor

mhegazy commented Apr 23, 2018

Possibly another use case of missing from #13195

@ganeshkbhat
Copy link

ganeshkbhat commented Jul 11, 2018

You could have or add something like this as well:

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

Reference:
#25570

@bradleyayers
Copy link
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
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants