Open ivogabe opened 10 years 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;
@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 _;
/* ----------------------------------- --- ---------------------------------- */
@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.
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 _;
/* ----------------------------------- --- ---------------------------------- */
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;
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>;
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.
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.
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
.
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.
// 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"
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() {}
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
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.
@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?
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 =)
@DanielRosenwasser this issue is 10yo. Do you think there is any chance of this being implemented in TypeScript soon?
Maybe @RyanCavanaugh can help answer this =)
It would be useful when a module can implement an interface using the
implements
keyword. Syntax:module MyModule implements MyInterface { ... }
.Example: