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

types(remix-server-runtime): make AppLoadContext an empty interface instead of any #1876

Merged
merged 5 commits into from
Aug 2, 2022
Merged

Conversation

DanielFGray
Copy link
Contributor

this allows users to override the context with their own definitions using ambient modules.

as an example:

declare module "remix" {
  interface AppLoadContext {
    graphql: <Result, Variables>(operation: {
      query: TypedDocumentNode<Result, Variables>
      variables: Variables
    }) => Promise<ExecutionResult<Result>>
  }
}

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Feb 10, 2022

Hi @DanielFGray,

Welcome, and thank you for contributing to Remix!

Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once.

You may review the CLA and sign it by adding your name to contributors.yml.

Once the CLA is signed, the CLA Signed label will be added to the pull request.

If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at [email protected].

Thanks!

- The Remix team

@ryanflorence
Copy link
Member

What does this do if you don't add that declare? Does it treat it as any or are you required to declare it?

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Feb 10, 2022

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

@DanielFGray
Copy link
Contributor Author

DanielFGray commented Feb 10, 2022

What does this do if you don't add that declare?

do you mean the declare in my example? it's my understanding that it's not a valid ambient module declaration without it

Does it treat it as any or are you required to declare it?

forgive me, I'm not sure I understand the question.

currently it's impossible to override the type for context that is passed to loader functions etc since AppLoadContext is defined as any, but defining it as an empty interface allows it to be an object that can be typed with ambient modules

@itsMapleLeaf
Copy link
Contributor

What does this do if you don't add that declare? Does it treat it as any or are you required to declare it?

If you don't add the declare, the type of context will be {}. This PR allows you to add a declare to change the context type to whatever you want. This is similar to declaring a type to extend the express request: https://stackoverflow.com/a/40762463/1332403

@0x221A
Copy link

0x221A commented Feb 10, 2022

This is how I currently override the type for context it's quite hard btw. I'm agreed with this PR since typescript will merge the interface with the same name within a module.

export interface AppLoadContext {}

/**
 * The arguments passed to ActionFunction and LoaderFunction.
 */
export type DataFunctionArgs<T extends (...args: any) => any> = Omit<Parameters<T>[0], 'context'> & {
  context: AppLoadContext
}
/**
 * A function that handles data mutations for a route.
 */
export interface ActionFunction {
  (args: DataFunctionArgs<RemixActionFunction>): ReturnType<RemixActionFunction>
}

@ryanflorence
Copy link
Member

ryanflorence commented Feb 10, 2022

Sorry, I'm not as well-versed in TypeScript as you might expect, I just know enough to get by.

I understand this PR enables you to do this:

declare module "remix" {
  interface AppLoadContext {
    graphql: <Result, Variables>(operation: {
      query: TypedDocumentNode<Result, Variables>
      variables: Variables
    }) => Promise<ExecutionResult<Result>>
  }
}

What if in my app I don't declare anything and I add load context and try to access it, what will the type be?

export let loader: LoaderFunction = async ({ context }) => {
  // what is context? Any? Can I access stuff on it w/o typescript getting mad?
  console.log("is TS mad?", context.foo);
}

I'd like TypeScript to treat it as any.

Another way to ask my question: does this PR now require an app to declare the AppLoadContext in order to use it without TypeScript yelling at you?

@0x221A
Copy link

0x221A commented Feb 10, 2022

What if in my app I don't declare anything and I add load context and try to access it, what will the type be?

That should be undefined since this just allows us to work with types easier. It's like javascript if the key doesn't exist in the object so that value should be undefined

@ryanflorence
Copy link
Member

ryanflorence commented Feb 10, 2022

I don't know why this question is so hard for me to ask and get an answer that makes sense to me 😅

With this PR, do I get red squiggly lines under context.foo if I don't declare an app load context type?

@airjp73
Copy link
Contributor

airjp73 commented Feb 10, 2022

I don't know why this question is so hard for me to ask and get an answer that makes sense to me 😅

With this PR, do I get red squiggly lines under context.foo if I don't declare an app load context type?

I think the answer is yes. Typescript playground example

@ryanflorence
Copy link
Member

Do you have to have an empty interface to be able to do this or can you do it if it's typed as any?

declare module "remix" {
  interface AppLoadContext {
    /* type here */
  }
}

@airjp73
Copy link
Contributor

airjp73 commented Feb 10, 2022

There are other things like namespaces that support declaration merging, but type does not. You get a "duplicate identifier" error (typescript playground).

@0x221A
Copy link

0x221A commented Feb 10, 2022

Do you have to have an empty interface to be able to do this or can you do it if it's typed as any?

You need to do that only if you want to add new properties to an interface. After that Typescript will merge the properties after you declare.

@itsMapleLeaf
Copy link
Contributor

Do you have to have an empty interface to be able to do this or can you do it if it's typed as any?

You cannot do what this PR wants if it's typed as any. Updating a library's types via declare module only works if the type being updated is an interface, and interfaces can't be any.

However, updating the PR with this would be close enough:

export interface AppLoadContext {
  [key: string]: any;
};

This would let you do context.something without errors, and allow it to be updated with declare module.

Slight downside: declare module can't erase the any, so the example in the OP would still let you do context.something without error. But I'd be willing to live with that.

@0x221A
Copy link

0x221A commented Feb 10, 2022

This PR is just for better DX since esbuild doesn't care what you declare in the type system 😂

@ryanflorence
Copy link
Member

@itsMapleLeaf ah, okay, I can maybe live with that? Don't like that it has to be an object now, but seems weird to be anything else.

@mjackson you have an opinion here?

@0x221A
Copy link

0x221A commented Feb 10, 2022

The other way is leaving it be any and using a passed-in type to the loader() and action() function but I don't see why to do that because AppLodeContext came from the server. Just declare once and use it anywhere.

@3x071c
Copy link

3x071c commented Mar 24, 2022

@ryanflorence Just came here after almost having opened a similar PR myself. This is the signature for AppLoadContext I'd recommend instead of an any type, which doesn't allow for declaration merging and therefore can't be strongly typed:

export declare interface AppLoadContext {
	[key: string]: unknown
};

This will continue allowing accessing any properties on AppLoadContext by default, but prevents unsafe property access by requiring type assertions at runtime. For those that prefer strict typing and know the structure of their context in advance, they can merge this generic interface declaration with their own.

@MichaelDeBoey MichaelDeBoey changed the title types: make AppLoadContext an empty interface instead of any types(remix-server-runtime): make AppLoadContext an empty interface instead of any Mar 28, 2022
@MichaelDeBoey
Copy link
Member

MichaelDeBoey commented Mar 28, 2022

@DanielFGray Code/type changes go against dev.
Could you please rebase your branch onto latest dev & set base branch to dev + resolve conflicts?

@MichaelDeBoey MichaelDeBoey added the needs-response We need a response from the original author about this issue/PR label Mar 28, 2022
@DanielFGray
Copy link
Contributor Author

I have updated this to use unknown, per @3x071c's suggestion

@MichaelDeBoey I think I rebased this properly

@github-actions github-actions bot removed the needs-response We need a response from the original author about this issue/PR label Apr 20, 2022
@marwan38
Copy link

Looks like it includes changes that weren't in this PR. Might want to reflog and or rebase again. Looking forwards to this being merged in as we have a similar use case.

@MichaelDeBoey
Copy link
Member

@DanielFGray Like @marwan38 said: the rebase included changes that doesn't belong in this PR.
Please remove these from your branch/PR.

@MichaelDeBoey MichaelDeBoey added needs-response We need a response from the original author about this issue/PR and removed needs-response We need a response from the original author about this issue/PR labels May 27, 2022
@justinwaite
Copy link
Contributor

@MichaelDeBoey just out of curiosity, at what point could we consider this stale or abandoned and give someone else a chance to get it across the finish line? I think this is a pretty large impact item for our company since our apps will all rely on this context. Let me know, I'm definitely willing to help!

@changeset-bot
Copy link

changeset-bot bot commented Jul 15, 2022

🦋 Changeset detected

Latest commit: 33fd91b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@remix-run/netlify Major
@remix-run/server-runtime Major
@remix-run/cloudflare Major
@remix-run/deno Major
@remix-run/dev Major
@remix-run/node Major
@remix-run/cloudflare-pages Major
@remix-run/cloudflare-workers Major
create-remix Major
@remix-run/architect Patch
@remix-run/express Major
@remix-run/vercel Major
@remix-run/serve Major
remix Major
@remix-run/eslint-config Major
@remix-run/react Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@@ -16,7 +16,7 @@ import { createServerHandoffString } from "./serverHandoff";

export type RequestHandler = (
request: Request,
loadContext?: AppLoadContext
loadContext: AppLoadContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this remain optional?

Suggested change
loadContext: AppLoadContext
loadContext?: AppLoadContext

@@ -18,7 +20,7 @@ export async function callRouteAction({
match,
request,
}: {
loadContext: unknown;
loadContext: AppLoadContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadContext: AppLoadContext;
loadContext?: AppLoadContext;

@@ -65,7 +67,7 @@ export async function callRouteLoader({
}: {
request: Request;
match: RouteMatch<ServerRoute>;
loadContext: unknown;
loadContext: AppLoadContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadContext: AppLoadContext;
loadContext?: AppLoadContext;

@@ -82,7 +82,7 @@ async function handleDataRequest({
serverMode,
}: {
handleDataRequest?: HandleDataRequestFunction;
loadContext: unknown;
loadContext: AppLoadContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadContext: AppLoadContext;
loadContext?: AppLoadContext;

@@ -180,7 +180,7 @@ async function handleDocumentRequest({
serverMode,
}: {
build: ServerBuild;
loadContext: unknown;
loadContext: AppLoadContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadContext: AppLoadContext;
loadContext?: AppLoadContext;

@@ -519,7 +519,7 @@ async function handleResourceRequest({
serverMode,
}: {
request: Request;
loadContext: unknown;
loadContext: AppLoadContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
loadContext: AppLoadContext;
loadContext?: AppLoadContext;

@MichaelDeBoey
Copy link
Member

@justinwaite If you're willing to actively working on this, you can always open a new PR, since @DanielFGray isn't responding for over a month now.

@DanielFGray
Copy link
Contributor Author

@justinwaite If you're willing to actively working on this, you can always open a new PR, since @DanielFGray isn't responding for over a month now.

@MichaelDeBoey I literally just pushed a new commit yesterday. I'm still working out some of the kinks since a lot of things changed since I first filed this, and I'm also having trouble running the test suite locally which I've been asking for help with in the Discord #contributions channel.

I'm very interested in having this merged and actively working fixing on the remaining failing tests.

@MichaelDeBoey
Copy link
Member

@DanielFGray Oh sorry, I looked over that commit, I only saw @justinwaite's message en then the Changeset message 🙈

@justinwaite Let's continue with this PR instead of creating a new one.

@marwan38
Copy link

marwan38 commented Jul 17, 2022

Out of curiosity, what do you need the tests for? Running a type check should be all you need since all that's being change is the type definitions.

EDIT: I just saw the pipelines, yes. You should be able to do tsc --noEmit or something in the root to run your type checks and make the necessary fixes.

@justinwaite
Copy link
Contributor

@DanielFGray Oh sorry, I looked over that commit, I only saw @justinwaite's message en then the Changeset message 🙈

@justinwaite Let's continue with this PR instead of creating a new one.

Yeah sorry about the confusion – after making that post I reached out to him on Discord, and the rest is history 😄

Happy to help in whatever way!

@pcattori
Copy link
Contributor

Hey all! Coming to this conversation a little late, but I've read through the approach here and it looks good 💯

We're putting some extra effort into type safety recently and this is the next thing on the docket. @DanielFGray let me know if there's any way I can help or support the awesome work you're doing! Happy to collaborate or support in whatever way.

DanielFGray and others added 5 commits August 1, 2022 18:12
@pcattori pcattori merged commit 43fbb62 into remix-run:dev Aug 2, 2022
mcansh pushed a commit that referenced this pull request Aug 11, 2022
… instead of `any` (#1876)

* make AppLoadContext an empty interface

* Update contributors.yml

* AppLoadContext interface uses unknown for missing values

* update types for AppLoadContext

* fix: make `AppLoadContext` parameter optional

parameter used to be implicitly optional as it used to be typed as `any`. it _is_ intended to be
optional, so this restores it as an optional param

Co-authored-by: Pedro Cattori <[email protected]>
@MichaelDeBoey MichaelDeBoey added the awaiting release This issue has been fixed and will be released soon label Aug 12, 2022
@MichaelDeBoey MichaelDeBoey removed the awaiting release This issue has been fixed and will be released soon label Aug 12, 2022
@huw
Copy link
Contributor

huw commented Aug 16, 2022

If anyone is visiting this issue after banging your head against the wall, if you choose to keep the module augmentation in a separate file, you also need to import @remix-run/server-runtime. The following is in a global.d.ts file:

import "@remix-run/server-runtime";

declare module "@remix-run/server-runtime" {
  interface AppLoadContext {
   key: "value"
  }
}

(You may also replace @remix-run/server-runtime with @remix-run/{adapter} if you import your loader function type declaration from the adapter package). I can then run the following in an index.tsx:

import type { LoaderFunction } from "@remix-run/cloudflare"

And everything is typed correctly. (Well, this change also makes context optional, which wasn’t necessarily the case in the past—does this mean I should be checking for its existence, or is it safe to assert that it’s present?)

@justinwaite
Copy link
Contributor

If anyone is visiting this issue after banging your head against the wall, if you choose to keep the module augmentation in a separate file, you also need to import @remix-run/server-runtime. The following is in a global.d.ts file:

import "@remix-run/server-runtime";



declare module "@remix-run/server-runtime" {

  interface AppLoadContext {

   key: "value"

  }

}

(You may also replace @remix-run/server-runtime with @remix-run/{adapter} if you import your loader function type declaration from the adapter package). I can then run the following in an index.tsx:

import type { LoaderFunction } from "@remix-run/cloudflare"

And everything is typed correctly. (Well, this change also makes context optional, which wasn’t necessarily the case in the past—does this mean I should be checking for its existence, or is it safe to assert that it’s present?)

I'm trying to address the optional issue here: #3989

eligundry added a commit to eligundry/remix-auth-spotify that referenced this pull request Jan 6, 2023
This undos the exact version change pushed in JosteinKringlen#282

- remix-auth is moved to `peerDependencies` as the documentation for
  remix-auth-spotify specifies that it should be manually installed.
- remix-auth is added to `devDependencies` for any tests that rely on
  remix have access to them.
- remix-auth-oauth2 is made into a package range that will support all
  1.x.x releases >= 1.5.0.

This change was made because installing as specifed in the documentation
lead to multiple versions of remix being installed which does not work
with any typescript type overloading that you might do.

remix-run/remix#1876

For example, I have an existing remix app using [email protected] with the
following in my remix.env.d.ts:

```typescript
// in remix.env.d.ts
import '@remix-run/server-runtime'

declare module '@remix-run/server-runtime' {
  interface AppLoadContext {
    requestId: string
  }
}
```

This allows me to access the `requestId` in a `loader` like so:

```
import { LoaderArgs  } from '@remix-run/node'

export async function loader({ context }) {
  foobar(context.requestId)
}
```

Installing remix-auth-spotify as specified in the docs caused
[email protected] to be installed alongside 1.7.4. This caused the type
overriding in Typescript to not work at all and make `context.requestId`
become unknown. This was resolved by bumping up remix to 1.9.0 in my
package.json. By using package ranges, downstream consumers of this
package are able to define the versions of remix.

Question: If accepted, would it make sense to also have
remix-auth-oauth2 moved to `peerDependencies`? I could envision an
authentication a situation where the upstream consumer of this package
has Spotify auth AND a custom OAuth2 setup. I don't have a super strong
stance, though.
eligundry added a commit to eligundry/remix-auth-spotify that referenced this pull request Jan 6, 2023
This undos the exact version change pushed in JosteinKringlen#282

- remix-auth is moved to `peerDependencies` as the documentation for
  remix-auth-spotify specifies that it should be manually installed.
- remix-auth is added to `devDependencies` for any tests that rely on
  remix have access to them.
- remix-auth-oauth2 is made into a package range that will support all
  1.x.x releases >= 1.5.0.

This change was made because installing as specifed in the documentation
lead to multiple versions of remix being installed which does not work
with any typescript type overloading that you might do.

remix-run/remix#1876

For example, I have an existing remix app using [email protected] with the
following in my remix.env.d.ts:

```typescript
// in remix.env.d.ts
import '@remix-run/server-runtime'

declare module '@remix-run/server-runtime' {
  interface AppLoadContext {
    requestId: string
  }
}
```

This allows me to access the `requestId` in a `loader` like so:

```typescript
import { LoaderArgs  } from '@remix-run/node'

export async function loader({ context }) {
  foobar(context.requestId)
}
```

Installing remix-auth-spotify as specified in the docs caused
[email protected] to be installed alongside 1.7.4. This caused the type
overriding in Typescript to not work at all and make `context.requestId`
become unknown. This was resolved by bumping up remix to 1.9.0 in my
package.json. By using package ranges, downstream consumers of this
package are able to define the versions of remix.

Question: If accepted, would it make sense to also have
remix-auth-oauth2 moved to `peerDependencies`? I could envision an
authentication a situation where the upstream consumer of this package
has Spotify auth AND a custom OAuth2 setup. I don't have a super strong
stance, though.
eligundry added a commit to eligundry/remix-auth-spotify that referenced this pull request Jan 6, 2023
This undos the exact version change pushed in JosteinKringlen#282

- remix-auth is moved to `peerDependencies` as the documentation for
  remix-auth-spotify specifies that it should be manually installed.
- remix-auth is added to `devDependencies` for any tests that rely on
  remix have access to them.
- remix-auth-oauth2 is made into a package range that will support all
  1.x.x releases >= 1.5.0.

This change was made because installing as specifed in the documentation
lead to multiple versions of remix being installed which does not work
with any typescript type overloading that you might do.

remix-run/remix#1876

For example, I have an existing remix app using [email protected] with the
following in my remix.env.d.ts:

```typescript
// in remix.env.d.ts
import '@remix-run/server-runtime'

declare module '@remix-run/server-runtime' {
  interface AppLoadContext {
    requestId: string
  }
}
```

This allows me to access the `requestId` in a `loader` like so:

```typescript
import { LoaderArgs  } from '@remix-run/node'

export async function loader({ context }) {
  foobar(context.requestId)
}
```

Installing remix-auth-spotify as specified in the docs caused
[email protected] to be installed alongside 1.7.4. This caused the type
overriding in Typescript to not work at all and make `context.requestId`
become unknown. This was resolved by bumping up remix to 1.9.0 in my
package.json. By using package ranges, downstream consumers of this
package are able to define the versions of remix.

Question: If accepted, would it make sense to also have
remix-auth-oauth2 moved to `peerDependencies`? I could envision an
authentication a situation where the upstream consumer of this package
has Spotify auth AND a custom OAuth2 setup. I don't have a super strong
stance, though.
@DanielFGray DanielFGray deleted the patch-1 branch May 24, 2023 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.