-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Type annotation for all exports in the module #38511
Comments
You can add Remix to this category https://blog.remix.run/p/remix-preview It will export a load function similar to Next.js's getServerSideProps |
I didn't notice this issue when I opened: #46942 There are a few more details for use cases and an alternative syntax, but I don't really care too much about the specific syntax. I just want the feature. :) You've got a 👍 from me! |
Adding SvelteKit to the list of frameworks that follows this pattern — this would be hugely beneficial to us as well. An even better outcome for users would be if modules could be typed implicitly — for example if certain files (either with a particular naming convention or because the framework generates a config that lists them) were known to implement a particular interface, the // src/routes/echo.ts
import { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = (request) => {
return {
status: 200,
headers: {},
body: JSON.stringify(request)
};
} Converting that to an explicitly typed module... // src/routes/echo.ts
-import { RequestHandler } from '@sveltejs/kit';
+import { Endpoint } from '@sveltejs/kit';
+
+export implements Endpoint;
-export const get: RequestHandler = (request) => {
+export function get(request) {
return {
status: 200,
headers: {},
body: JSON.stringify(request)
};
} ...would only really start paying dividends once you had multiple exports, whereas an implicitly-typed module would provide significant benefits even to people who see the red squigglies but otherwise have no idea what TypeScript is: // src/routes/echo.ts
-import { RequestHandler } from '@sveltejs/kit';
-export const get: RequestHandler = (request) => {
+export function get(request) {
return {
status: 200,
headers: {},
body: JSON.stringify(request)
};
} |
Hey @Rich-Harris 👋 I'd also love implicit types based on a convention and the tsconfig-based configuration of that convention (😆). That would be a big benefit for Remix users as well. I'm concerned that this would stall the feature. If there's some way to have both and start with the explicit that's the future I would prefer personally. I think explicit has a place in the TS ecosystem as well, so both make sense to me. So if we could get the explicit finished and start benefitting from that then work on the implicit that would be super :) |
Indeed. If feature B depends on feature A, it's better to implement feature A first rather than waiting to implement both. Nevertheless, it's very useful in my experience to understand the rough shape of feature B (and any other dependent features you have in mind) before designing feature A, lest you end up with a design that for whatever reason makes those follow-ups hard or impossible. For that reason it's often worthwhile to include those considerations in the initial discussion. |
+1 from Cloudflare Pages as well! We declare this {
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
} With support for type-checking of modules, we could declare a export implements PagesFunctions
export const onRequestPost = async ({ request }) => {
const { name } = await request.json()
return new Response(`Hello, ${name}!`)
}
export const onRequestGet = () => {
return new Response("Hello, user!")
} But this API would do exactly what we'd need! Thanks everyone for suggesting it :) |
@Rich-Harris love your idea of implicit naming based on file pattern, imo in tsconfig something like: {
"compilerOptions": {
"exportTypings: {
"*.svelte.ts": "@sveltejs/kit::Endpoint"
}
}
} Maybe with something less c++-y than I think "functions/**/*.ts":"@cloudflare/workers-types::PagesFunctions" I'm liking this idea more and more... |
Notes from the last time we talked about this: #38713 and some of the possible consensus #420 (comment) |
Thanks @orta. Looks like there's community interest and team appetite for the idea. So where do we go from here? How do we push things forward? |
Mentioned in other threads on the topic, Storybook's Component Story Format also heavily relies on this pattern, where the default export includes metadata about a component, and each named export is a component example, or story: import { Meta, Story } from '@storybook/react';
import { Button } from './Button';
const meta: Meta = {
title: 'Demo/Button',
component: Button;
};
export default meta;
export const Primary: Story = () => <Button primary>Primary</Button>;
export const Disabled: Story = () => <Button disabled>Disabled</Button>; In our case, users also want more type safety and better autocompletion than the general types provide, so they will parameterize them by the props of the component that is being documented. So at the top of each file they might make specific versions at the top of each file: type Meta = ComponentMeta<typeof Button>;
type Story = ComponentStory<typeof Button>;
const meta: Meta = { ... };
export default meta;
export const Primary: Story = // ...; Ideally a solution would be powerful enough to express this kind of specificity. So this would fit the bill in each file: export implements CSF<typeof Button>; But this proposal in its current form would not: "src/**/*.stories.tsx":"@storybook/react::CSF" A combination of the former (for users who want more control) and the latter (for convenience) would be amazing. 💯 |
In this spirit, I just wondered about the following as a possible future extension of this proposal: I am not sure about the syntax and how to implement this but maybe someone can come up with something smart. Taking Remix as an example, this would enable e.g. // Internal Remix code
interface EntryRouteModule<LoaderData = Response | AppData> {
CatchBoundary?: CatchBoundaryComponent;
ErrorBoundary?: ErrorBoundaryComponent;
default?: RouteComponent;
handle?: RouteHandle;
links?: LinksFunction;
meta?: MetaFunction | HtmlMetaDescriptor;
action?: ActionFunction;
headers?: HeadersFunction | { [name: string]: string };
loader?: LoaderFunction<LoaderData>;
} Then, in a route // routes/posts.tsx
import type { EntryRouteModule } from 'Remix'
import { useLoaderData } from 'Remix';
type Post = { title: string };
export implements EntryRouteModule<{ posts: Post[] }>;
// this is typed correctly because of the line above
export const loader = async () => {
return {
posts: await postsQuery()
}
export default function Blog() {
// Have TypeScript figure this out automatically somehow:
// useLoaderData now knows that `LoaderData === Post[]`. There is no need to `useLoaderData<Post[]>`
const { posts } = useLoaderData()
return (
<ul>
{posts.map(post => (
<li>{post.title}</li>
))}
</ul>
)
} This is a slightly different use case than the one outlined in the initial Next.js example, where the inference of import { PageModule } from 'next'
type Post = { title: string };
export implements PageModule<{ posts: Post[] }>;
// Here, `posts` is being inferred correctly as per proposal
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li>{post.title}</li>
))}
</ul>
)
}
export async function getStaticProps() {
return {posts: []}
}
export default Blog |
@DanielRosenwasser just let me know that it's possible to import self to simulate what we're asking for. Just in case you're curious this is how it would look like in Next.js for instance: // @file pages/posts.tsx
import React from 'react';
// This can be more complex to cover getServerSideProps etc
type InferPropTypes<T> = T extends { getStaticProps: () => Promise<{ props: infer U }> }
? U
: never;
import * as self from './posts';
const Posts: React.FC<InferPropTypes<typeof self>> = ({ posts }) => {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
);
};
export async function getStaticProps() {
const posts: Array<{ title: string }> = [];
return { props: { posts } };
}
export default Posts; This is less automatic than typing all exports using something like |
I would also love to see such a feature in TypeScript. I don't think that there is a new syntax needed. What if TypeScript could use a Currently e.g. a file.d.ts declare export someVariable: number file.js export someVariable = 'Hello' So if TypeScript would use the information from the With a solution like we can see in SvelteKit where types are generated and then linked with the real source files via the
|
That's a stroke of genius @ivanhofer — it solves the problem very elegantly. I've often wondered why My conclusion was that TypeScript probably assumes that I wonder if a new compiler option like |
Yes usually the Using the declaration files to type the corrresponding source file makes only sense for your own source files (everything that matches "include" from your A compiler option where someone could opt-in into the Such a feature would also make it possible to have "scoped global types" (not sure how to call them). A concrete example for SvelteKit would be the |
@ivanhofer With your suggested solution, would that I mean I need to create a |
@camjackson I haven't used Remix yet, so I don't fully know how route files work there. I just looked at some examples in the comments above. |
I suggested this on the Remix discord and it was explained to me that it won't work. In Remix, route modules include both frontend and backend code, and the framework needs to be able to tree shake / code split the code apart. So its conventions must be export-based, they can't be fields of a single object. That's why this proposal is needed. |
I agree, having a config option to choose the behaviour where
Well, from a technical perspective Remix could also transform the file and split it up into distinct files during the build process. Maybe a bit overkill, but it could already work with the current TypeScript version. |
This would be HUGE 🔥 |
This could also be helpful for globals that are only exposed in certain files, like Node environment variables and test frameworks. |
3 questions:
|
As an interested user, here are my opinions:
|
Next.js 13 solution to mitigate for the lack of this feature (+ more):
|
@kentcdodds export const foo = 1
export const obj: { bar?: string } = {}
export type Exports = typeof export; // { foo: number; obj: { bar?: string } } I don't have an immediate use case for it but I'm sure this can be useful |
Put this at your module file and File: myModule.ts /* myModule.ts */
/* ------------------------------- Type Check ------------------------------- */
type _myModule = typeof import('./myModule');
const __MODULE_TYPE_CHECK__: MyModuleInterface = {} as _myModule; Ugly? Yes, but it works. If your module does not There is no cyclical-dependency since the import is "garbage collected" and is not transpiled. |
Handy trick to help error if your exported types are wrong. However, it doesn't help with auto-complete nor will it report errors in a convenient location. |
Not ideal, but for auto completion: const MODULE: MyModuleInterface = {
someProperty: ...,
someMethod(param) { }
}
export const someMethod = MODULE.someMethod or const MODULE: MyModuleInterface = {
someProperty: ...,
someMethod(param) { }
}
export const { someProperty, someMethod } = MODULE; |
I'd like to add another use-case for this feature. We use Currently, we rely on seeding Actions with skeleton templates that use jsdoc annotations to power the intellisense. This works but means that the majority of the template is consumed by non-functional boilerplate. Supporting typed modules such as in this proposal would give us (and I suspect all other serverless-like platforms / tools) an improved way to deliver a more robust and elegant DX for our customers / users. |
One syntax option: export {} satisfies Type This would be a shorthand for typing exports of the entire module: export {} satisfies RemixRoute<{ bookId: string }>
export function action({ requesr }) // Did you mean "request"?
export function loader({ params }) {
params.bookID // Did you mean "bookId"?
} It's important to keep in mind that the One benefit of this would be that you are also typing the syntax that marks a module as a module, and TypeScript could emit it |
One thing I don't like so much about the approaches that involve new syntax is that it requires author of the subject module to opt in. From what I understand of the use-cases that motivate this issue, the types for module exports are not optional. Instead, these types are being enforced by an external factor or framework. I think it would be interesting if the enforcement could thus come from outside too. From that angle, I wonder if it might be interesting to think about a design where these module-level type signatures are 'attached' to files through the A straw-man idea follows. Here, we can define a set of mappings from files globs to a module specifier and exported symbol that defines the expected shape of the matching files' exports. {
"exportTypes": [{
"include": ["**/pages/*.ts"],
"typePath": "@framework/types",
"typeName": "PageExports",
}],
} In other words, any file matching the glob The side-benefit is that affected files no longer need to include superfluous syntax to benefit from this feature. Many frameworks area already providing default |
I like that. Though in Remix at least, you can very dynamically define what files count as routes so I would like an additional capability to define a type for the exports. I'm a fan of what @jamiebuilds-signal suggests with the satisfies keyword. |
I see the benefit of that, especially the idea of it being enforced from the outside, because it's a framework-level expectation. My concern is that it makes the code much less discoverable. To me it's very important that a new developer can look at a piece of code, without any context, and be able to figure out what it does by following the breadcrumbs. I prefer an API that explicitly establishes a link that says "this code that you're looking at implements this interface", and then you can click through to see what that interface is. As opposed to it being configured somewhere else, with no way of like, naturally discovering it from the code in question. It becomes something that you just have to know. |
That's a great point. Perhaps the relevant comparison here is what you have defined in your |
allowing ambient module declarations to specify relative module names as an alternative to using tsconfig could be a more intuitive and/or powerful way to achieve this by way of using typescript (especially import { type RemixRoute } from 'remix';
declare module './MyRoute' {
export {} satisfies RemixRoute<{ bookId: string }>
} and this could work with wildcards as well import { type PageExports } from 'next';
declare module './**/pages/*.ts' {
export {} satisfies PageExports
} a more drastic change, module declarations themselves could satisfy types declare module './MyRoute' satisfies RemixRoute<{ bookId: string }>
declare module './**/pages/*.ts' satisfies RemixRoute<{ bookId: string }> |
Oh nice, @btoo that's very clever! |
I like |
@btoo Is that doable at the moment or is that a a feature request? |
Objectifs de la PR : - Retirer toute les classes du projet pour plus être orienté vers de la programmation fonctionnelle - Créer un système d'auto-loading beaucoup plus propre avec des fichiers séparer les uns des autres - Utilisation d'arrows functions a la place de regulars functions - Nouvelle convention de nommage des fichiers *(kebab-case, fonctionnalité du fichier défini en `file.functionnalité.ts`, export barrel pour les choses pas auto load)* Pour l'instant les exports sont un peu défini de manière magique, mais dans le futur cette PR viendra fix ça : microsoft/TypeScript#38511
I would really like this too, and would be happy with A workaround while we wait for this feature is to import the module into itself and then check that what is imported satisfies some type. For example: // myModule.ts
export default function () {
return "test";
}
import * as __SELF__ from './myModule';
__SELF__ satisfies { default(): string }; This is quite cumbersome though, since you need to specify the name of the file in the file, and you get the error in the satisfies line, not on the export line. |
### What? Expose the `MiddlewareConfig` interface. ### Why? You can now `import type { MiddlewareConfig } from "next/server"` to type the `config` object in your `middleware.ts` file. Now you an type the entire file for example like so: ```ts // middleware.ts import type { NextMiddleware, MiddlewareConfig } from "next/server" export const middleware: NextMiddleware = async (req) => { //... } export const config: MiddlewareConfig = { //... } ``` ### How? Re-exported the interface from its current location via `server/web/types`, to colocate it with `NextMidldeware`. I wonder if we could somehow type this file automatically, but it might be dependent on microsoft/TypeScript#38511 Closes NEXT-2308 [Slack thread](https://vercel.slack.com/archives/C03S9JCH2Q5/p1706287433026409?thread_ts=1706058855.423019&cid=C03S9JCH2Q5), [Slack thread](https://vercel.slack.com/archives/C03KAR5DCKC/p1706659724141899)
Search Terms
Export, typed exports, modules, Next.js, Redwood.js
Suggestion
Allow a new syntax From issue #420 that will allow typing all of exports in a module.
Use Cases and Examples
Increasingly frameworks are using named module exports as a way of organizing their code. For instance Next.js is using named exports to specify things like exported component and functions for fetching data:
Frameworks relying on named export
Checklist
My suggestion meets these guidelines:
Related issues
The text was updated successfully, but these errors were encountered: