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

New Syntax: module implements Type #46942

Closed
5 tasks done
kentcdodds opened this issue Nov 29, 2021 · 11 comments
Closed
5 tasks done

New Syntax: module implements Type #46942

kentcdodds opened this issue Nov 29, 2021 · 11 comments
Labels
Duplicate An existing issue was already created

Comments

@kentcdodds
Copy link

kentcdodds commented Nov 29, 2021

Suggestion

πŸ” Search Terms

  • module interface type
  • module implements
  • module type

βœ… Viability 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, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

I want to add a new syntax for setting export types for a module:

import type {MyType} from './types'

module implements MyType

Shorthand syntax:

module implements {MyType} from './types'

Default type export shorthand syntax:

module implements {default} from './types'

Note: I'm much more interested in the feature than the syntax.

πŸ“ƒ Motivating Example

I want to be able to do this:

// types.ts
export type Math = {
  add: (a: number, b: number) => number
  subtract: (a: number, b: number) => number
  multiply?: (a: number, b: number) => number
  pi: number
}

// my-math-module.ts
import type {Math} from './types'

module implements Math // <-- this is the feature I want

export function add(a, b) { // <-- the types are set by and checked against the module implements syntax
  return a + b
}

export function subtract(a, b) { // <-- the types are set by and checked against the module implements syntax
  return a - b
}

// extra exports are fine, they just don't inherit the type
export function divide(a: number, b: number): number {
  return a / b
}

// optional types do not have to be exported, so no multiply here

// not just functions!
export const pi = 3.14

See, you can have a conventional module format and get automatic typings for everything the module is supposed to export.

πŸ’» Use Cases

Some libraries have conventions for modules. I thinking specifically of Remix, but other frameworks have similar conventions (like Next.js, SvelteKit, and Storybook). For example, in Remix a route module should export the following interface:

interface EntryRouteModule {
  CatchBoundary?: CatchBoundaryComponent;
  ErrorBoundary?: ErrorBoundaryComponent;
  default?: RouteComponent;
  handle?: RouteHandle;
  links?: LinksFunction;
  meta?: MetaFunction | HtmlMetaDescriptor;
  action?: ActionFunction;
  headers?: HeadersFunction | { [name: string]: string };
  loader?: LoaderFunction;
}

The issue here is that as a user of the convention, I have to type everything I export. This comes with two problems:

  1. If I forget, I don't have anything running at compile time to tell me I forgot.
  2. It's extra typing (both figuratively and literally)
  3. If what I'm exporting is a function, I can't easily use a function declaration which I prefer.

So, for example, I currently have to do this:

import {type LoaderFunction} from 'remix'

export const loader: LoaderFunction = async ({request}) => {
  return {url: request.url}
}

What I want to be able to do is this:

module implements {RouteModule} from 'remix'

export async function loader({request}) {
  return {url: request.url}
}

Please let me know if you need any other details. Thanks for taking the time to read and consider!

@kentcdodds kentcdodds changed the title New Syntax: module implements interface Type New Syntax: module implements Type Nov 29, 2021
@spencersmb
Copy link

Love any time saving features like this personally. Love TS, hate having to type it out so much haha.

@eyelidlessness
Copy link

This would be particularly useful for gradually typing a JS codebase. I’ve been trying to find a solution to use declaration files for codebases I maintain (as opposed to node_modules dependencies, where declarations work as expected) instead of JSDoc, and even the most effective solutions I’ve come up with still require JSDoc on everything.

Some bikesheds to consider:

  • Because this could be useful for that gradual typing use case, @module might be a way to express it
  • Triple-slash references could be used to express the full typings for a given module
  • The paths mapping already types whole modules from declaration files on import and could be used to check the module itself

@MartinJohns
Copy link
Contributor

Duplicate of #38511.

@Saeris
Copy link

Saeris commented Nov 29, 2021

I could see a similar case for a GraphQL service with type generation from a schema.

Example, let's consider the following graphql type:

./schema.gql

type Movie {
  id: ID!
  name: String!
  tagline: String
  overview: String!
  releaseDate: DateTime
  runtime: Int
  cast(limit: Int): [Person!]!
  crew(limit: Int): [Person!]!

  recommended(
    page: Int= 1
    limit: Int
  ): [Movie!]!
  similar(
    page: Int = 1
    limit: Int
  ): [Movie!]!
}

Which would generate the following type:

./generated/Movie.d.ts

import { Parent, Context, Info } from "./resolverTypes"
import { Person } from "./Person"

export interface Movie {
  id: string;
  name: string;
  tagline?: string;
  overview: string;
  releaseDate?: Date;
  runtime?: number;
  cast: (parent: Parent, args: { limit: number; }, context: Context, info: Info) => Person[]
  crew: (parent: Parent, args: { limit: number; }, context: Context, info: Info) => Person[]

  recommended: (parent: Parent, args: { page?: number; limit: number; }, context: Context, info: Info) => Movie[]
  similar: (parent: Parent, args: { page?: number; limit: number; }, context: Context, info: Info) => Movie[]
}

Now if you had a module that exported resolvers for this type, you could then automatically type the exports like so:

./src/resolvers/movieResolvers.ts

import  { type Movie } from "../../generated/Movie"

module implements Partial<Movie>

export const cast = (_, { limit }) => { /* ... */ }

export const crew = (_, { limit }) => { /* ... */ }

export const recommended = (_, { page = 1, limit }) => { /* ... */ }

export const similar = (_, { page = 1, limit }) => { /* ... */ }

In this case I would want to be able to have a partial implementation, so that my named exports are automatically typed but I don't get any errors for missing exports (I may have default resolvers for those, for example). Maybe for the example above, when another module imports this one, Typescript would be smart enough to know that movieResolvers.ts exports cast, crew, recommended, and similar, but does not export the other values from the Movie type. Whereas if we hadn't used Partial<T> here, we'd have some error saying required exports are missing.

Just my two cents. Would like to hear what others have to say before commenting further. Neat idea @kentcdodds!

@kettanaito
Copy link

That's a great proposal. Framework and library writers can benefit from this greatly!

On a technical level, I wonder if such module validation falls into the way TypeScript works. From my limited understanding, TypeScript takes the files you define in files/include in tsconfig.json and validates the module tree, those included types acting as entries. Much like module resolution, in order for modules to be validated, they must be imported. The easiest way to represent this is the following:

// file.ts

// TypeScript can annotate export types for "*.ext"
// based on your custom "declare module" declarations.
import data from './module.ext'

// TypeScript validates the types you declare
// immediately in this module.
type MyType = {}

// TypeScript validates the types imported from other
// modules (as long as they fall under the "include" paths.
import type { ExternalType } from './extraneous'
function action(a: ExternalType) {}

Neither of these behaviors would satisfy the proposed module-wide validation.

If I understand the intention correctly, the idea is for a certain group of modules to have a typed export type. Since the surface at which such validation occurs doesn't equal the consumer's surface (you're not expected to import foo from './routes/foo'), this proposal likely means implicit type validation, which I'm not sure is a thing in TypeScript.

The closest thing in terms of behavior is annotating export types of imported modules:

// global.d.ts
// Whenever you "import X from "foo.png"...
declare module "*.png" {
  const content: string
  // ...anotate its default export as a string.
  export default content
}

That does, however, imply that *.png module is being imported somewhere along the modules tree, which is not how the proposed feature should behave. I'd like to learn more about whether there's such an implicit type validation in TypeScript and whether it could be utilized to implement what Kent suggests.

@dechowdev
Copy link

dechowdev commented Nov 29, 2021

I must admit - I like the idea

But it seems specific for the generation of module interfaces, where it's (within the proposal) implied in the code rather than a separate definition file

I understand the sentiment

But is the idea maybe diluting the concept of types and interfaces and their separation with definition files?

I think the crux of the situation is more within the maintenance aspect rather than a syntax implementation necessarily?

@ubmit
Copy link

ubmit commented Nov 29, 2021

Duplicate of #38511.

Although they are similar, the proposed syntax is different

@intrnl
Copy link

intrnl commented Nov 29, 2021

Although they are similar, the proposed syntax is different

Sure the proposed syntax is different, but the two issue ultimately talks about the same idea πŸ™‚

@MartinJohns
Copy link
Contributor

@GuilhermedeAndrade

Although they are similar, the proposed syntax is different

As @intrnl said, they're the same idea.

Quoting RyanCavanaugh:

we prefer to organize issues by use case / end-goal, not have competing separate issues for different syntactic representations.

@cevr
Copy link

cevr commented Nov 29, 2021

Great proposal! This would greatly bridge the gap in the DX between developing in typescript with classes and a more module based approach.

Personally, I prefer to use modules instead of classes, so this sort of feature is sorely missed.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 29, 2021
@RyanCavanaugh
Copy link
Member

Closing & locking due to twitter incoming traffic that hasn't seen there's already an issue for this. Thanks!

@microsoft microsoft locked as resolved and limited conversation to collaborators Nov 29, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests