Skip to content

Type annotation for all exports in the module #38511

Open

Description

@mohsen1

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.

export implements {
  default: string
  foo: (bar: boolean) => number
}

export default "Hello world!";
export function foo(bar) { return 1 }

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:

import { PageModule } from 'next'

type Post = { title: string };
export implements PageModule<{ posts: Posts[] }>;

function Blog({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

export async function getStaticProps() {
  return { props: { posts: [] } }
}

export default Blog

Frameworks relying on named export

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Related issues

Activity

  1. balazsorban44 commented on May 12, 2020

    @balazsorban44

    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

  2. kentcdodds commented on Nov 29, 2021

    @kentcdodds

    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!

  3. Rich-Harris commented on Nov 30, 2021

    @Rich-Harris

    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 export implements (or whatever) would be unnecessary (unless you needed to specify type variables). For example a typical SvelteKit endpoint looks like this:

    // 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)
      };
    }
  4. kentcdodds commented on Nov 30, 2021

    @kentcdodds

    Hey Rich Harris (@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 :)

  5. Rich-Harris commented on Dec 1, 2021

    @Rich-Harris

    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.

  6. GregBrimble commented on Dec 1, 2021

    @GregBrimble

    +1 from Cloudflare Pages as well!

    We declare this PagesFunction type in our @cloudflare/workers-types package, and encourage people to include it in their types in tsconfig.json:

    {
      "compilerOptions": {
        "types": ["@cloudflare/workers-types"]
      }
    }

    With support for type-checking of modules, we could declare a PagesFunctions or similar:

    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 :)

  7. geelen commented on Dec 1, 2021

    @geelen

    Rich Harris (@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 :: but you get the idea.

    I think <module-name>.<exports-types>.ts could become a fairly common convention. But equally, for all-one-type projects you could just set *.ts to be any type you like, or for Pages we could set a subdir like:

    "functions/**/*.ts":"@cloudflare/workers-types::PagesFunctions"

    I'm liking this idea more and more...

  8. orta commented on Dec 8, 2021

    @orta
    Contributor

    Notes from the last time we talked about this: #38713 and some of the possible consensus #420 (comment)

  9. kentcdodds commented on Dec 10, 2021

    @kentcdodds

    Thanks Orta Therox (@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?

  10. shilman commented on Dec 16, 2021

    @shilman

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

  11. hollandThomas commented on Jan 7, 2022

    @hollandThomas

    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.

    In this spirit, I just wondered about the following as a possible future extension of this proposal:
    A module that implements named exports may infer the types of any imports that originate from the same module that provides the named export interface.

    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 posts === Post[] does not rely on any additional imports.

    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
  12. mohsen1 commented on Jan 19, 2022

    @mohsen1
    ContributorAuthor

    Daniel Rosenwasser (@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 export implements but it's possible to do today.

  13. ivanhofer commented on Mar 25, 2022

    @ivanhofer

    I would also love to see such a feature in TypeScript.
    Probably every project that has file conventions would benefit from such a feature. My project typesafe-i18n could also benefit from it.

    I don't think that there is a new syntax needed. What if TypeScript could use a .d.ts file to check what variables and exports a file should have?

    Currently e.g. a file.d.ts would tell every other file that imports file.js what values it exports and what shape those variables and functions have. But in that file.js no type-check happens. The file.js could also export something totally different and TypeScript would not throw an error:

    file.d.ts

    declare export someVariable: number

    file.js

    export someVariable = 'Hello'

    So if TypeScript would use the information from the d.ts file to check if the .js file was implemented correctly, we could have a solution to the typed export problem. Of course this should also work with .ts files.

    With a solution like we can see in SvelteKit where types are generated and then linked with the real source files via the rootDirs compiler option, those .d.ts files could be generated without adding declaration files inside the actual src folder.

    Having those checks enabled would probably break a few existing code bases if they manually create .d.ts files. Or actually they would get notified that they defined something wrong ;)
    But this is a topic for a later discussion. I just want to propose an alternative to this thread.

  14. 21 remaining items

  15. kentcdodds commented on Dec 20, 2022

    @kentcdodds

    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 Jamie (@jamiebuilds-signal) suggests with the satisfies keyword.

  16. camjackson commented on Dec 21, 2022

    @camjackson

    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 tsconfig.json and equivalent language service constructs.

    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.

  17. ggoodman commented on Dec 21, 2022

    @ggoodman

    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.

    That's a great point. Perhaps the relevant comparison here is what you have defined in your tsconfig.json for the compilerOptions.lib option. This is something defined on the 'outside' that affects which ambient types will show up. For use-cases where you want to be explicit, you can opt into a triple-slash directive to make the dependency explicit.

  18. btoo commented on Dec 21, 2022

    @btoo
    Contributor

    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 satisfies) rather than json

    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 ambitious feature, module declarations themselves could satisfy types

    declare module './MyRoute' satisfies RemixRoute<{ bookId: string }>
    
    declare module './**/pages/*.ts' satisfies RemixRoute<{ bookId: string }>
  19. ggoodman commented on Dec 21, 2022

    @ggoodman

    Oh nice, btoo that's very clever!

  20. mohsen1 commented on Dec 23, 2022

    @mohsen1
    ContributorAuthor

    I like export {} satisfies Type because satisfies works better than implements in this case. The only thing is the emitted export {} in runtime space that might not be desired.

  21. EloB commented on Jan 23, 2023

    @EloB

    btoo Is that doable at the moment or is that a a feature request?

  22. added a commit that references this issue on Feb 25, 2023
  23. mariusGundersen commented on Jun 1, 2023

    @mariusGundersen

    I would really like this too, and would be happy with export {} satisfies Type, although some magic with tsconfig and wildcard paths would be very useful.

    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.

  24. nfroidure commented on Dec 3, 2024

    @nfroidure

    Would be nice to be able to create some .d.ts files too with this:

    declare module '*.command.ts' satifies/extends/implements {
      name: string;
      definition: CommandDefinition;
      default: Command;
    }
    declare module '*.handler.ts' satifies/extends/implements {
      method: string;
      path: string;
      operation: OpenAPIOperation;
      default: Handler;
    }

    A bit like https://github.com/vercel/next.js/blob/canary/packages/next/image-types/global.d.ts but for module validation.

    Whook relies a lot on named imports (to declare handlers / services / commands), it would be a significant improve for the developer experience.

  25. juancampa commented on Dec 26, 2024

    @juancampa

    We've been following this issue for years (from membrane.io). It would greatly simplify how our users interact with the platform.

    Mohsen Azimi (@mohsen1) Feel free to add us to the list 🙏

  26. Brennername commented on Mar 21, 2025

    @Brennername

    @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 :: but you get the idea.

    I do not mind using the :: "operator" because that is common in java method references as well, and it clearly indicates you are dealing not with a call using the dot operator but a reference using the :: operator.

    But as I read this I was thinking "Why is this not a feature?"

  27. bgub commented on Jul 9, 2025

    @bgub
    // tsconfig.json
    {
      compilerOptions: {
        declarationValidations: {
          "**/*.route.ts": "@remix/type-assertions/route.d.ts",
          "**/*.model.ts": "./models/_type.d.ts"
        }
      }
    }

    Just following up on this issue! My favorite solution is Glen Maddern (@geelen)'s proposal (quoted example above). This would be incredibly useful and I'd be happy to help contribute.

  28. AgentEnder commented on Oct 17, 2025

    @AgentEnder

    Throwing Nx's name in the mix as frameworks / tools that would benefit from this

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

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions