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

Allow a module to implement an interface #420

Open
ivogabe opened this issue Aug 10, 2014 · 62 comments
Open

Allow a module to implement an interface #420

ivogabe opened this issue Aug 10, 2014 · 62 comments
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@ivogabe
Copy link
Contributor

ivogabe commented Aug 10, 2014

It would be useful when a module can implement an interface using the implements keyword. Syntax: module MyModule implements MyInterface { ... }.

Example:

interface Showable {
    show(): void;
}
function addShowable(showable: Showable) {

}

// This works:
module Login {
    export function show() {
        document.getElementById('login').style.display = 'block';
    }
}
addShowable(Login);

// This doesn't work (yet?)
module Menu implements Showable {
    export function show() {
        document.getElementById('menu').style.display = 'block';
    }
}
addShowable(Menu);
@sophiajt
Copy link
Contributor

How would this work with external modules? It's likely once people can use it for internal, they'll also want to use it with external.

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 15, 2014

That's a good question. I don't know which syntax would be the best, but here are a few suggestions:

implements Showable; // I would prefer this one.
module implements Showable;
export implements Showable;

It should only be allowed on external modules that don't use an export assignment, since if you use an export assignment, the thing that you export can already have an implements on another place.

@sophiajt sophiajt added this to the Community milestone Aug 15, 2014
@RyanCavanaugh
Copy link
Member

Approved. We prefer the syntax

export implements Showable;

and agreed that this is unneeded for files export = assignments.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 15, 2014

Some more questions:

  • Since we are allowing modules to have types at declaration sites, should not we allow them to have types at use sites as well. e.g.:
declare module "Module" implements Interface { }

import i : Interface = require("Module");
  • What do you do with merged declarations, should you enforce the interface on the aggregate of all declarations? and what happens if they do not match in visibility?
    e.g.:
module Foo {
    export interface IBar {
        (a:string): void;
    }

    export module Bar implements IBar {  // should this be an error?
        export interface Interface {}
    }    

    function Bar(a: string) : void { }  // not exported
}

var bar: Foo.IBar = Foo.Bar;

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 15, 2014

It should be allowed on ambient external modules. For these modules two syntaxes should be allowed in my opinion:

declare module "first" implements Foo { }
declare module "second"  {
  interface Bar { }
  export implements Bar; // this syntax is necessary, with the first syntax you can't reference Bar.  
}

Or should Bar be in the scope in an implements clause before the opening {?

Adding type info to an import statement isn't really useful in my opinion, since you can add the type info to the module itself.

And for merged declarations, I'd say that the module block that contains the implements clause should implement the interface. That also prevents issues with visibility.

@jbondc
Copy link
Contributor

jbondc commented Apr 14, 2015

How would this be related to #2159? A namespace implements an interface?

@DanielRosenwasser
Copy link
Member

@jbondc If we had this, it would also apply to namespaces. You should think of internal modules and namespaces as isomorphic.

@Elephant-Vessel
Copy link

Are you sure you want to go down an implementational path where "namespaces" can implement interfaces?

@joelday
Copy link
Contributor

joelday commented Jan 2, 2017

Oh wow, this has been approved for quite a while. @RyanCavanaugh, @DanielRosenwasser, @mhegazy unless you have any second thoughts or tweaks, I'll probably implement this soonish.

@Elephant-Vessel
Copy link

I withdraw my previous skepticism, I actually exited for the new structural possibilities it would bring.

In line with that, please consider enforcing the interface of the aggregate of the interface instead of only the block that declares the implementation - The nature of namespaces/modules is to be spread out and to contain a lot of non-trivial components. I'd like to be able to use this, but I certainly don't want to define my whole namespace/module in the same file. Why not just use a class in that case?

@aluanhaddad
Copy link
Contributor

@Elephant-Vessel I'm not sure if we are talking about Modules, or Namespaces, or Packages, or Features, or...

@Elephant-Vessel
Copy link

@aluanhaddad What do you mean?

@aluanhaddad
Copy link
Contributor

I mean that at the time that this discussion started module didn't mean what it means today. We now use the term namespace to refer to what is described in the OP as a module, while module has taken on a more precise and incompatible meaning. So when you talk about multiple files taking part in this implementation are you referring to namespaces or modules?

@OliverJAsh
Copy link
Contributor

Unlike regular objects, modules can hold types as well as values. Would it be possible annotate a module to specify that it must export a specific type?

For example, we are defining modules for each of our API endpoints. Each module must export types Params and Response. These will be different for each module, but we would like to enforce that each module exports something under those names. Furthermore, if we renamed the types, ideally TS would update the name in every module.

@bwittenberg
Copy link

bwittenberg commented Oct 6, 2021

I also want a mechanism to specify that a module implements an interface by using syntax like:

export implements Showable

Here's a stack overflow article with some tips on the subject written by @RyanCavanaugh : https://stackoverflow.com/a/16072725

Hopefully this feedback helps the TS team prioritize work. I'm sure there's lots to do!

@EloB
Copy link

EloB commented Feb 3, 2022

Is this doable at all now? I really want to type my Next.js pages. This one has been opened a long time, 8 years... Anyone know the thoughts on TS team opinion on this?

@shaipetel
Copy link

I have found a sorta-workaround.
You will have to assign your module into a variable, or declare a global of the interface's type.

interface IUtilities {
    Version: string;
    Build: number;
    PrintVersion(): string;
}
module Utilities {
    export var Version = "1.1";
    export var Build = 23;
    export function PrintVersion() {
        return `v${Version}b${Build}`;
    }
}
//by assigning it into a var of the interface type, we provoke TypeScript to check the module comply with the interface
var p: IUtilities = Utilities;

@EloB
Copy link

EloB commented Feb 4, 2022

@shaipetel thanks for sharing but this doesn’t support default exports?

@shaipetel
Copy link

@EloB the problem with default exports, that you do not include inside a namespace or module - is that the point of import doesn't guarantee to import all members. So can't verify it against an interface, I guess (it is not all or nothing, in a sense of all members/functions or none).
Because of tree shaking I guess - think about it, if you were to verify it as an interface:

  • You export 3-4 functions
  • Your caller imports it and TypeScript says "Your imports are of type X
  • Your caller only uses 2 functions inside
  • Tree shaking will drop the rest unused code << at this point you'll have a problem since TypeScript "lied" about the type you have...

I'm assuming in a nutshell, that's one of the problems. You can't guarantee the entire module will be consumed/exported and won't be broken up by the build.

@reaktivo
Copy link

reaktivo commented Feb 4, 2022

@shaipetel Typescript is orthogonal to tree-shaking, meaning that it will still Typecheck unused code paths.

@shaipetel
Copy link

@reaktivo exactly, but that is what can cause the problem.
You see, when you import only some members of a module A1 that adheres to interface iA,
then import other members from a different module A2 that also adheres to interface iA.

Now in your code - TypeScript will tell you A1 and A2 both implement iA, right?

So, assume you have a function (not inside your code, maybe a global one. example would be if you try to JSON.stringify your import) that expects iA as a parameter.

TypeScript would basically tell you its ok to send either your imported A1 or A2, without knowing which members you chose to import, and which members are needed by your function, in during dev - TypeScript gives the green light assuming everything would be there.
During build, your A1 is missing some members from iA, and A2 is missing other members...

The build wouldn't know what that function expects, and how it is going to use the parameter - so tree shaking won't be able to identify the members that are needed (dependencies).

I hope I'm making sense...

@EloB
Copy link

EloB commented Feb 5, 2022

So by changing tree shaken to follow module requirement (optional) would fix everything?

@JarnoRFB
Copy link

@EloB disregarding tree shaking, the same could be done for normal modules using

interface IUtilities {
  Version: string;
  Build: number;
  PrintVersion(): string;
}


const Version = "1.1";
const Build = 23;
const PrintVersion = () => `v${Version}b${Build}`;


// by assigning it into a var of the interface type,
// we provoke TypeScript to check the module comply with the interface.
const module: IUtilities = {
  Version, Build, PrintVersion,
};

export default module;

@EloB
Copy link

EloB commented Sep 7, 2022

@JarnoRFB Thanks for your time. This doesn't work tree shaking right? I already know that you can write like this but from my understanding it won't work with tree shaking?

@ericmasiello
Copy link

@EloB disregarding tree shaking, the same could be done for normal modules using

interface IUtilities {
  Version: string;
  Build: number;
  PrintVersion(): string;
}


const Version = "1.1";
const Build = 23;
const PrintVersion = () => `v${Version}b${Build}`;


// by assigning it into a var of the interface type,
// we provoke TypeScript to check the module comply with the interface.
const module: IUtilities = {
  Version, Build, PrintVersion,
};

export default module;

Yes, but also, we can't simply disregard tree shaking.

@JarnoRFB
Copy link

JarnoRFB commented Sep 8, 2022

@EloB my understanding is unfortunately to limited to comment on that. I just adapted your solution for normal modules.

@antl3x
Copy link

antl3x commented Nov 24, 2022

Put this at your module file and __TYPE_CHECKING__ will do the job.

File: YourModule.ts

/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

type _ = typeof import('./YourModule');
const __TYPE_CHECKING__: YourModuleInterface = {} as _;

/* ----------------------------------- --- ---------------------------------- */

Ugly? Yes, but it works.

If your module does not export const ... and does not respect your interface, you will receive a type error.

There is no cyclical-dependency since the import is "garbage collected" and is not transpiled.

EDIT:

Here a more clear (semantical?) variable name example to avoid misinterpretations.

/* myModule.ts */

/* ------------------------------- Type Check ------------------------------- */

type _myModule = typeof import('./myModule');
const _myModuleImplements: MyModuleInterface = {} as _myModule;

@ericmasiello
Copy link

@nthypes, thanks for posting this. I'm okay with ugly if it works. However, I must be missing something because this doesn't seem to work for me:

/* commands.ts */
/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

interface YourModuleInterface {
    foo: string;
}


type _ = typeof import('./commands');
export const foo: YourModuleInterface = "hello" as _;

/* ----------------------------------- --- ---------------------------------- */

I also tried this but this also doesn't work.

/* commands.ts */
* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

interface YourModuleInterface {
    foo: string;
}


type _ = typeof import('./commands');
export const __TYPE_CHECKING__: YourModuleInterface = { foo: "hello" } as _;

/* ----------------------------------- --- ---------------------------------- */

@antl3x
Copy link

antl3x commented Nov 24, 2022

@ericmasiello the implementation is wrong.

You should not edit __TYPE_CHECKING__ variable value. Only the type to check.

Should error (wrong type):

/* commands.ts */

export interface YourModuleInterface {
    foo: string;
}

export const foo = false

/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

type _ = typeof import('./commands');
const __TYPE_CHECKING__: YourModuleInterface = {} as _;

/* ----------------------------------- --- ---------------------------------- */

Should error (no export):

/* commands.ts */

export interface YourModuleInterface {
    foo: string;
}

const foo = false

/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

type _ = typeof import('./commands');
const __TYPE_CHECKING__: YourModuleInterface = {} as _;

/* ----------------------------------- --- ---------------------------------- */

Should pass:

/* commands.ts */

export interface YourModuleInterface {
    foo: string;
}

export const foo = 'false'

/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */

type _ = typeof import('./commands');
const __TYPE_CHECKING__: YourModuleInterface = {} as _;

/* ----------------------------------- --- ---------------------------------- */

PS: I also added a more "semantical" version on my original post.

@ericmasiello
Copy link

Got it. Thanks, @nthypes. Adding on to this, the variable named __TYPE_CHECKING__ is arbitrary. You can call it anything you want. At first, I thought there might be some magical/special meaning associated with it, but that's not the case. Here's my updated example with some comments that elaborate on what I believe is happening based on the code you provided.

/*
Inside the file commands.ts
@note we need to import the same file we're in (`commands.ts`) in the module type checking section below
*/

// this defines the interface for our module (commands.ts), i.e., what it must export
export interface CommandsModuleInterface {
    // put whatever you want here for your use case.
   // This use case says we must export a value named `foo` of the type `string`
    foo: string;
}

// here, we implement our interface
export const foo = "hello";

/* -------------------------------------------------------------------------- */
/* Below this line is the "hack" to validate our command.ts module.
/* It works by importing the `type` of the module (file) we're in, i.e., `commands.ts`
/* -------------------------------------------------------------------------- */

// this infers the type of what we're *actually* exporting from `commands.ts`
// we store this as a type called `_` (again this is arbitrary)
type _ = typeof import('./commands');

// The line below here is where the actual type checking occurs.
// We assign an arbitrarily named `const` as `__MODULE_TYPE_CHECK__`
// and specify the type as our desired module interface, `CommandsModuleInterface`.
// We assign the `const` `CommandsModuleInterface` a value of `{}` but immediately
// try to type assert that`__MODULE_TYPE_CHECK__`, which we said should be of type
//  `CommandsModuleInterface`, matches the type actually exported by our module
// and assigned the type `_`.
// @note The eslint-disable-next-line is optional. I needed it for my lint rules
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __MODULE_TYPE_CHECK__: CommandsModuleInterface = {} as _;

/* ----------------------------------- --- ---------------------------------- */

@antl3x
Copy link

antl3x commented Nov 24, 2022

Also if you want auto-completion with intellisense:

const MODULE: MyModuleInterface = {
    someProperty: ...,
    someMethod(param) { }
}

export const someMethod = MODULE.someMethod

or

const MODULE: MyModuleInterface = {
    someProperty: ...,
    someMethod(param) { }
}

export const { someProperty, someMethod } = MODULE;

@alloy
Copy link
Member

alloy commented Dec 2, 2022

This is great. I've amended the example slightly to not emit an empty runtime object:

type THIS_MODULE = typeof import('./commands');
type TYPE_CHECK<T extends CommandsModuleInterface> = T;
declare const _: TYPE_CHECK<THIS_MODULE>;

@lorenzodallavecchia
Copy link

Too bad you have to hard-code the file name in the file itself. There is a high chance of it getting misaligned after renaming/copying.

@antl3x
Copy link

antl3x commented Dec 2, 2022

Too bad you have to hard-code the file name in the file itself. There is a high chance of it getting misaligned after renaming/copying.

If this happens you will receive an type error.

ctjlewis added a commit to SpellcraftAI/yield-stream that referenced this issue Mar 3, 2023
@antl3x
Copy link

antl3x commented Aug 5, 2023

UPDATE: I'm using a more generic implementation to reduce the amount of boilerplate per file. For those interested:

/* satisfies.ts */
/* satisfies.ts https://github.com/microsoft/TypeScript/issues/420 */

/* eslint-disable */

/* -------------------------------------------------------------------------- */
/*                            Module Type Checking                            */
/* -------------------------------------------------------------------------- */
// This is a hack to make sure that the module type is correct

export const satisfies = <TypeOfFile>() => <ModuleInterface extends TypeOfFile>() => {
  void 0 as ModuleInterface;
};
/* UserManagement.ts */
import { satisfies } from 'satisfies'

interface IUserManagement {
  addUser: () => boolean
}

export const addUser = () => true

satisfies<typeof import("./UserManagement.ts")>()<UserManagement>();

Of course, you can change it to a name that makes the most sense to you: satisfies | assert | implements.

@fdaciuk
Copy link

fdaciuk commented Apr 9, 2024

This is a pretty old issue, but so relevant! Would be nice to have this feature in TypeScript :D

I created the issue #58029, but since it's the same subject, I'll give my 2 cents here:

My proposal is adding a way to define a module interface, and a way to use it, adding the module interface and module implements keywords.

📃 Motivating Example

// file user-module-interface.ts
export module interface UserModuleInterface {
  async function getUser(id: string): Promise<User>
  async function createUser(): Promise<void>
}

// ---------------

// file prisma-user.ts
import type { UserModuleInterface } from "./user-module-interface"

module implements UserModuleInterface

export async function getUser(id) {}

export async function createUser() {}

// ---------------

// another-file.ts
import { getUser, createUser } from "./prisma-user"

Explaining the example

First thing we have to do is creating a module interface. It can be exported from a specific file as type or used in the same file that functions will be implemented.

Everything inside a module interface must be implemented when module implements is used.

Then you can import (as a type) this module interface into another file and use module implements to implement the interface. This can be a line module implements MyInterface or something with a body, like:

// file prisma-user.ts
import type { UserModuleInterface } from "./user-module-interface"

module implements UserModuleInterface {
  export async function getUser(id) {}
  export async function createUser() {}
}

We can also implement more than one interface, like in classes:

// file article-module-interface.ts
export module interface ArticleModuleInterface {
  function getArticles(): Promise<Article[]>
}

// ---------------

// file prisma-user.ts
import type { UserModuleInterface } from "./user-module-interface"
import type { ArticleModuleInterface } from "./artictle-module-interface"
module implements UserModuleInterface, ArticleModuleInterface

export async function getUser(id) {}

export async function createUser() {}

export async function getArticles() {}

Rules

The module that is using module implements must implement all functions defined in module interface. Otherwhise, TypeScript should throw an error.

Shouldn't be a problem to implement other functions that are not defined in a module interface, as long as we implement at least every function defined in module interface.

We can possibly make some functions optional (not required to this suggestion):

// file user-module-interface.ts
export module interface UserModuleInterface {
  async function getUser(id: string): Promise<User>
  // createUser is optional
  async function createUser?(): Promise<void>
}

// ---------------

// file prisma-user.ts
import type { UserModuleInterface } from "./user-module-interface"

module implements UserModuleInterface

export async function getUser(id) {}

// no errors, even if `createUser` is not implemented

💻 Use Cases

In this proposal, we do not rely on runtime features. All types (module interface and module implements) can be safely removed in build time.

The functions defined inside a module that extends a module interface can be auto-inferred.

And with this implementation, we have tree-shaking, because we can import only the functions we'll use.

To do something similar today, we have to create a class or an object and export the whole object, even if we want to use only one function.

@JarnoRFB
Copy link

@fdaciuk Definitely think that modules implementing interfaces / types would warrant a special syntax to do away with the clunky workarounds presented in the thread. However, why would we need a special module interface construct for that and could not just use the normal interface / type keywords we already have?

@fdaciuk
Copy link

fdaciuk commented Apr 16, 2024

I don't think we need @JarnoRFB. Maybe just a simple interface will solve the problem. module interface was just a suggestion to give more semantic, but like you said, probably module implements is enough =)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests