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

[Proposal] External Module Type #13231

Closed
11 of 25 tasks
SomaticIT opened this issue Dec 30, 2016 · 18 comments
Closed
11 of 25 tasks

[Proposal] External Module Type #13231

SomaticIT opened this issue Dec 30, 2016 · 18 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@SomaticIT
Copy link

SomaticIT commented Dec 30, 2016

Introduction

Currently, TypeScript allows to ensure typings on differents levels using type, interface or class.
There are numbers of scenarios where we want to ensure typings on the module level.

Scenario 1: Module Interface

Many libraries can be extended using plugins which are loaded as modules.
These libraries' maintainers could define an interface which should be implemented by the module.

// mylib.d.ts
export interface IPlugin {
    init(): void;
    build(context: PluginContext): boolean | PromiseLike<boolean>;
    dispose?: () => void;
}

//plugin.ts
import { IPlugin, PluginContext } from "mylib";

export implements IPlugin;
                    ^^^ ERR: Module does not implement "IPlugin". Missing members: "init".

export function build(context: PluginContext) { return false; }

Scenario 2: Module proxy

Imagine a library which abstract RPC services. On the server part, you define services as modules and on the client part, you get Promised proxies of these services which you can use to remote call your service methods.

// myrpclib.d.ts
export type Deferred<T> = { [P in keyof T]: Promise<T[P]>; };
export function createClient<T>(url: string): Deferred<T>;

// services/entity.service.ts
export function getAll(): Promise<Entity[]> { /* ... */ }
export function getOne(id: string): Promise<Entity> { /* ... */ }
export function create(entity: Entity): Promise<Entity> { /* ... */ }
export function update(entity: Entity): Promise<Entity> { /* ... */ }
export function remote(id: string): Promise<void> { /* ... */ }
export function sync(id: string): Entity { /* ... */ }

// clients/client1.ts
import { createClient } from "myrpclib";
type RemoteService = typeof module("../services/entity.service");
const client = createClient<RemoteService>("http://host/entity.service");

Scenario 3: Inline module typing

By extending the preceding scenario, we could imagine that the library also provides a module loader plugin for its managed services.

// myrpclib.d.ts
declare module "rpc!*" { 
    const res: any;
    export default res;
}

// clients/client2.ts
type RemoteService = typeof module("../services/entity.service");
import client from "rpc!host/entity.service" as Deferred<RemoteService>;

Scenario 4: Wildcart module typing

This time, we have a library that proxy services in a Web Worker.

// myrpclib.d.ts
declare module "worker!*" { 
    const res: Deferred<typeof module("*")>;
    export default res;
}

// clients/client3.ts
type RemoteService = typeof module("../services/entity.service");
import client from "worker!../services/entity.service";

client.getAll().then(/* ... */);
client.sync().then(/* ... */);
// client type is "Deferred<typeof module("../services/entity.service")>";

Language Features

Syntatic

What is the grammar of this feature?

  • export implements IPlugin (could be written implements IPlugin)
  • typeof module("module/path")
  • import myModule from "myModule" as Type
  • typeof module("*") in wildcart module definition

Are there any implications for JavaScript back-compat? If so, are they sufficiently mitigated?

  • No backward compatibility implications

Does this syntax interfere with ES6 or plausible ES7 changes?

  • No conflicting keywords

Semantic

What is an error under the proposed feature? Show many examples of both errors and non-errors

//export implements...
// Same as an interface

//typeof module("module/path");
type Moduletype = typeof module("module/path"); // OK
type UnknownModuleType = typeof module("unknown/module"); 
                                          ^^^^  // COMPILER ERROR: "unknown/module" does not exists

const test: typeof module("module/path");  //OK
function test(): typeof module("module/path"); //OK
function load(name: string): typeof module(name); //OK

//import module as
import myModule from "myModule" as Type; //OK
import * as myModule from "myModule" as Type; //OK
import * as unknown from "unknown" as Type; //OK
import { someMember } from "myModule" as Type;
                                          ^^^^  // COMPILER ERROR: Only default and global import are allowed!

//typeof module("*")
declare module "proxy!*" { 
    const res: typeof module("*"); // OK
    export default res;
}

type myType = typeof module("*"); 
                            ^^^^  // COMPILER ERROR: module "*" does not exists

How does the feature impact subtype, supertype, identity, and assignability relationships?

  • Moderate impact
  • Modules should be treated as special Interfaces (or special Classes) so they could be used in same contexts and they can interact we others.
  • An import could be typed

How does the feature interact with generics?

  • Limited impact
  • If a module will become a type (like an Interface is), it should interact with generics.

Emit

What are the effects of this feature on JavaScript emit? Be specific; show examples

  • No effect

Does this emit correctly in the presence of variables of type ‘any’? Features cannot rely on runtime type information

  • No emit difference

What are the impacts to declaration file (.d.ts) emit?

  • Limited impact
  • declaration file should contain export implements... informations

Does this feature play well with external modules?

  • Exclusively for external modules

Compatibility

Is this a breaking change from the 1.0 compiler? Changes of this nature require strong justification

  • New keywords are introduced but it is only typings utilities

Is this a breaking change from JavaScript behavior? TypeScript does not alter JavaScript expression semantics, so the answer here must be “no”

  • No

Is this an incompatible implementation of a future JavaScript (i.e. ES6/ES7/later) feature?

  • No

Other

Can the feature be implemented without negatively affecting compiler performance?

  • Yes

What impact does it have on tooling scenarios, such as member completion and signature help in editors?

  • Moderate impact

TODO

  • Get validation from the community
  • Get feedback from the community and improve spec
  • Add more exemples if needed
  • Implementation details
@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Dec 30, 2016
@DanielRosenwasser
Copy link
Member

One thing about this proposal is that it only enforces a contract between a module and a type - in other words, it only guarantees the values that exist on the module.

Something I would kind of like to see is the idea of a contract on the shape of the module (or namespace) as a whole - a guarantee that a module contains certain values, types, and namespaces. There are definitely other languages that do this, so it's not unheard of.

That said, we don't necessarily have to go down that route.

@SomaticIT
Copy link
Author

Indeed, a module interface could shape the entire module (with types and nested namespaces).

But I think a module should also implements a basic type/interface.

Maybe a module interface syntax to separate both ?

@mhegazy
Copy link
Contributor

mhegazy commented Dec 30, 2016

  • Module implementing an interface is already tracked by Allow a module to implement an interface #420. and it is marked as "Accepting PRs".

  • typeof module("module/path"), i do not see why not, but the syntax should be different, we do not want it to conflict with Proposal: Get the type of any expression with typeof #6606 (which is also marked as "Accepting PRs"). it would also be useful for scenarios in Explore supporting ES import() proposal  #12364, where module name is computed dynamically.

  • import myModule from "myModule" as Type not sure why is this useful, the scenario should be covered by either typeof module or by declaring an ambient external module.

  • typeof module("*") the module wild card feature was not meant to be a parametric module import. the main issue here is that the compiler does things in phases, and module resolution happens much earlier than type checking, and by that time modules are not there any ways. so i do not think we can do this without re-architectecting the compiler.

@zpdDG4gta8XKpMCd
Copy link

dup

i strongly oppose this in favor of unifying modules (namespaces) with objects per #8358

@aluanhaddad
Copy link
Contributor

This also relates to #11611

@masaeedu
Copy link
Contributor

@Aleksey-Bykov Even if you unify namespaces with objects, you need additional syntax to ascribe types to external modules (for which the "implementation" is not neatly enclosed, but is instead supplied by scattered export statements).

@aluanhaddad
Copy link
Contributor

@masaeedu I believe such types already exist. Consider

// module-a.ts
export default function () {}
export const a = 1;
export let b = 'hello';

// module-b.ts
import * as moduleA from './module-a';

export const a: typeof moduleA = {
  a: moduleA.a,
  b: moduleA.b,
  default: moduleA.default
};

@masaeedu
Copy link
Contributor

masaeedu commented Sep 19, 2017 via email

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Sep 19, 2017

@masaeedu, not sure i am following, consider an example below:

export type Ns<T> = {
    type List = List<T>;
    interface Data {
       list: List;
    }
    value: number;
}
export const ns: Ns<string> = {
    value: 1;
};
const list: ns.List = { ... };
const data: ns.Data = { list };

@masaeedu
Copy link
Contributor

masaeedu commented Sep 19, 2017 via email

@masaeedu
Copy link
Contributor

I do not want to deconstruct Foo and scatter it throughout my module definition as type annotations on my export statements. To give an analogy, this is the equivalent of a user asking for object types const obj: { x: string }, and another person saying well you don't need those, you can just do const x: string; const obj = { x }. Sure, in some cases that is fine, but it doesn't subsume the original use case.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Sep 19, 2017

How do I ensure the sum total of everything I am exporting in myfancymodule is compatible with Foo

well, as far as the syntax it could have looked like this

namespace myfancymodule : Foo {
}
export = myfancymodule 

or better yet (if we unify objects and namespaces)

const myfancymodule : Foo = {
}
export = myfancymodule 

or simply and more idiomatic

export namespace myfancymodule : Foo {
}

(although it would be a sub-module)

as to the original topic it indeed requires new special syntax/keywords within a module to indicate it complies to an interface:

implements Foo;
export function ...;

@aluanhaddad
Copy link
Contributor

@Aleksey-Bykov it depends. Although namespaces and objects are unfortunately not unified, you can still, by convention establish a type for an export in a single place by creating a declaration and exporting it.

@masaeedu I see what you are getting at now. You want to be able to declare the interface mandated by a module and then receive an error if the contract is broken.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Oct 2, 2017

@aluanhaddad although a type can be exported it cannot be closed over other types and it's a huge maintainability pain, consider

interface T<A extends B, B extends C, C extends D, D extends E, E extends F, F> {
   type X = (value: A | B | C) => D;
   type Y = (value: B | C | D) => E;
   type Z = (value: C | D | E) => F;
}
namespace T {
   export type X<A extends B, B extends C, C extends D, D> = (value: A | B | C) => D;
   export type Y<B extends C, C extends D, D extends E, E> = (value: B | C | D) => E;
   export type Z<C extends D, D extends E, E extends F, F> = (value: C | D | E) => F;
}

@aluanhaddad
Copy link
Contributor

@Aleksey-Bykov that would be very valuable, I think there is an issue open for it actually, #17588, and I was going to write that if objects and namespaces were unified as you propose, that the above behavior would be a pleasant, natural consequence, but I see you have already stated just that there 😁

@Ciantic
Copy link

Ciantic commented Apr 17, 2018

With code splitting working with await import I have an use case where I would like to create e.g. a function that takes in arbitrary import, such as:

const dosometstuff = async (moduleToBeImported: typeof module) => {
    let imported = await import(moduleToBeImported);
    // ...
}

// Usage...
await dosometstuff("./SomeModule");

Naturally above does not work yet, but it would be nice to typecheck that "./SomeModule" indeed is a module. Perhaps with some constraints on it too, for example that it exports at least Component of certain type.

And btw, if the module weren't some magic the import() could be defined using that. E.g.

declare function import<T>(m: Module<T>): Promise<T>

@Jessidhia
Copy link

It seems this is already mostly satisfiable with the typeof import("...") construct?

@SomaticIT
Copy link
Author

@Jessidhia You're right. I close the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants