microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.16k stars 12.38k forks source link

Allow a module to implement an interface #420

Open ivogabe opened 10 years ago

ivogabe commented 10 years ago

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);
antl3x commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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.

antl3x commented 1 year ago

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

/* 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
/**
 * # Example
 * ```typescript UserManagement.ts
 *
 * import { satisfies } from 'satisfies'
 *
 * interface IUserManagement {
 *  addUser: () => boolean
 * }
 *
 * export const addUser = () => true
 *
 * satisfies<IUserManagement, typeof import('./test')>;
 * ```
 */

export function satisfies<U, T extends U>() {
  return 0 ?? (void 0 as T);
}
/* UserManagement.ts */
import { satisfies } from 'satisfies'

interface IUserManagement {
  addUser: () => boolean
}

export const addUser = () => true

satisfies<IUserManagement, typeof import("./UserManagement.ts")>; // very smal runtime footprint - only import statment

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

fdaciuk commented 5 months ago

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 commented 4 months ago

@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 commented 4 months ago

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

fdaciuk commented 3 months ago

@DanielRosenwasser this issue is 10yo. Do you think there is any chance of this being implemented in TypeScript soon?

fdaciuk commented 2 months ago

Maybe @RyanCavanaugh can help answer this =)