Skip to content

Multiple projects with overlapping dependency graphs and different typings #21965

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

Closed
OliverJAsh opened this issue Feb 15, 2018 · 9 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Feb 15, 2018

I have an app with two TS (tsconfig.json) projects:

  • client: a browser app which uses the built-in typings from the dom lib
  • server: a Node app which uses the types package @types/node

The dependency graph of these projects overlap slightly, as we have components that are server and client rendered. We will refer to files in this overlap as "shared".

I want to be able to access both client-only and server-only constructors in shared files, e.g. window and global.

However, this is not possible today, as the server/client TS projects would emit type errors on window or global access, respectively.

The only way this will work today is if both client and server TS projects specify typings for Node and the browser. However, this is problematic because the typings do not correspond with the environment the code will run in, and mistakes are easily made, such as:

  • referencing window from code that is only ever ran through Node (server), where window does not exist
  • referencing window from code that is ran on through both Node (server) and the browser (client), but without guarding for Node, where window does not exist

Ideally, TypeScript would allow shared files to reference typings in either TS project, but only after guarding first. For example:

// shared/Count.tsx

const getCount = () => {
  // `window` access is not available without guarding first
  if (typeof window !== 'undefined') {
    // `window` access is available here.
    return window.localStorage.getItem('count');
  } else {
    return undefined;
  }
};

// React component that is used on the client and server

const Count: SFC<{}> = () => {
  const count = getCount();

  if (count !== undefined) {
    return <div>Count is ${count}</div>;
  } else {
    return <div>Count unavailable</div>;
  }
};

Similarly, Node's global would only be available after guarding first.

@yortus
Copy link
Contributor

yortus commented Feb 16, 2018

I wonder if something like #14052 would help? I think this would be a great practical use case to add to the discussion there. More flexibility with manipulating the global environment through types would be great.

@OliverJAsh
Copy link
Contributor Author

@yortus Have you got any ideas how this would be possible if we had #14052?

@yortus
Copy link
Contributor

yortus commented Feb 16, 2018

@OliverJAsh it wouldn't solve this issue as it is currently proposed, but I think a better approach to globals could solve both that and this issue, and others like #10050.

Actually, could you do something like this now?

// In your server project only:
// node.d.ts is present in this build, so `Global` and `global` will be fully defined
interface Window {[x: string]: any; }
declare let window: Window | undefined;

// In your client project only:
// dom.d.ts is present in this build, so `Window` and `window` will be fully defined
interface Global {[x: string]: any; }
declare let global: Global | undefined;

// In your shared code (compiled into both client and server builds):
window.x; // ERROR: Object is possibly 'undefined'
if (typeof window !== 'undefined') {
    window.x; // OK
}
global.x; // ERROR: Object is possibly 'undefined'
if (typeof global !== 'undefined') {
    global.x; // OK
}

Provided all your global accesses go through properties on window or global, both projects should build, and one of the builds will issue errors if there are property reference typos, or if any globals are referenced directly.

@OliverJAsh
Copy link
Contributor Author

@yortus Good idea, and one that @samijaber also raised offline.

I think that would get us some of the way, however T | undefined is not truthful: in environments where window or global don't exist, they won't resolve to undefined, but rather the program will throw a ReferenceError if you try to reference them as they are not declared.

Declaring them as T | undefined allows you to reference the object, but properties may only be referenced after guarding. Ideally, TypeScript wouldn't allow you to even reference the object:

window; // ERROR: Window does not exist in all environments
if (typeof window !== 'undefined') { // Safe to use the reference with `typeof`
    window.x; // OK
}

@yortus
Copy link
Contributor

yortus commented Feb 16, 2018

Yes right. That would indeed be something new in TypeScript. Even optional properties can be referenced, so optional globals wouldn't help. You basically want to be able to blacklist an identifier, like having a 'negative' declaration.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 16, 2018

The lib file, be it node or dom is just a declaration file. nothing stops you from creating your own lib file with var window: Window | undefined. and same for global in node.
I find it hard to see how we would do this in a configurable fashion.

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Feb 16, 2018
@OliverJAsh
Copy link
Contributor Author

OliverJAsh commented Feb 17, 2018

@mhegazy

creating your own lib file with var window: Window | undefined

That would still allow window to be accessed in the server/Node project, which would be a ReferenceError in JavaScript. We need TypeScript to prevent all usage of the declaration until its existence has been guarded with typeof window !== 'undefined'.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@OliverJAsh
Copy link
Contributor Author

@mhegazy Did you have any thoughts in response to my last comment? I would like to re-open this issue, as the suggested workaround does not help.

@microsoft microsoft locked and limited conversation to collaborators Jul 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants