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

Generic namespaces #19728

Closed
billba opened this issue Nov 3, 2017 · 16 comments
Closed

Generic namespaces #19728

billba opened this issue Nov 3, 2017 · 16 comments
Labels
Duplicate An existing issue was already created

Comments

@billba
Copy link
Member

billba commented Nov 3, 2017

REVISED: My initial proposal was wrong, here is a revised version.

I have a library that exports a collection of interrelated interfaces, classes, and functions, which operate collaboratively on a single generic type. For instance, you can use them to perform operations on bot messages, game actions, or IoT events, but typically you"d only ever be working with one of those at a time. So the fact that the library is generic gives it broad appeal to a wide variety of developers, but is not itself of value to a given developer. (Contrast with RxJS where solid generic support has specific value to a given developer, because they will likely use it with a wide variety of types in a given project.)

An unfortunate consequence of the way the components of this library come together is that they are sometimes very resistant to type inference, so many invocations end up requiring a type decoration, which creates an enormous amount of visual noise. It"s a terrible developer experience.

What I have now is:

// Library
export class Class<T> {
}
export function func<T>(..) {
}

// Library Consume
import { Class, Interface, func  } from "my-library";

// frequently need to spell out Class<Bot>, Interface<Bot>, and func<Bot>

Type aliases help a little, because I can do:

type BotInterface = Interface<Bot>;

But there is no equivalent for functions and classes. And even if there were, that would still make it painful to write my library, because it would be using generics everywhere when what I really want to do is write my library with just a single top-level generic. And it would still make it overly painful to consume my library, because aliases would have to be manually created for every component in the library. What I want is a generic namespace:

// Library
export namespace<T> {
    export class Class {
    }
    export function func(...) {
    }
}

// Library Consumer
import { MyNamespace } from "my-library";
const { Class, Interface, func } = MyNamespace<Bot>;

// just use Class, Interface, and func

Workarounds: I tried mimicking this by creating a generic factory class, but it is a huge hack and not powerful enough. There is a good reason namespaces exist in TypeScript, and adding generics would make them much more powerful. As it stands, the only real workaround -- which I have been forced to take, and is obviously unsustainable and unmaintainable -- is to fork the library into one version for each type.

@ghost
Copy link

ghost commented Nov 3, 2017

Maybe allowing a type alias in a class would help here?

class MyStackModule<T> {
    type Stack = T[];
    create(): Stack { return []; }
    push(s: Stack, value: T) { s.push(value); }
    pop(s: Stack): T | undefined { return s.pop(); }
}

// Instantiate the module like an ocaml functor. Now everything will work on `number`.
const NumStack = new MyStackModule<number>();
const stack: NumStack.Stack = NumStack.create();
NumStack.push(stack, 1);
const one = NumStack.pop(stack);

More practically, it might help to know why type inference isn"t working for func.

@billba
Copy link
Member Author

billba commented Nov 4, 2017

(Please note that I revised my original proposal above)

The functions are mostly just helper object factories to cut down on typing/noise. Here"s a cartoon of the library:

// Library
export class Class<T> {
    constructor(private lambda: (t: T) => void);
    twice(): Class<T> {
       return new Class<T>(t => {
           this.lambda(t);
           this.lambda(t);
        });
    }
    static makeTwice<T>(lambda: (t: T) => void): Class<T> {
        return new Class(lambda).twice();
    }
}

export function func<T>(lambda: (t: T) => void) {
    return new Class<T>(lambda);
}

// Library consumer
import { Class, func } from "my-library";

// the consumer creates lots of objects like this
const object = func<Bot>(t => t.reply("I hear you, brother"));
// or
const object = func((t: Bot) => t.reply("I hear you, brother"));

There"s no way to infer the Bot type when object is created. It has to be declared somewhere so that lambda has typed access to t.reply(). And it it a characteristic of the way this library is used that there could be a lot of these. And, again, the type will always be the same for a given library consumer. The poor bot developer has to litter her code with <Bot>.

This also might explain why allowing a type alias in a class won"t help. I"m not using classes to build data structures. I"m using them as bags of methods that operate on lambdas whose parameters are always the same type. There are several classes, and they need access to each"s constructors, static methods, and types.

Here"s the way I want to write the above:

export namespace MyLibrary<T> {

export class Class {
    constructor(private lambda: (t: T) => void);
    twice(): Class{
       return new Class(t => {
           this.lambda(t);
           this.lambda(t);
        });
    }
    static makeTwice(lambda: (t: T) => void): Class {
        return new Class(lambda).twice();
    }
}

export function func(lambda: (t: T) => void) {
    return new Class(lambda);
}

}

Much cleaner without all those extra generics.

@billba billba changed the title Generic namespaces and variable aliases Generic namespaces Nov 4, 2017
@aluanhaddad
Copy link
Contributor

Seems like the general problem is generic variables.

@billba
Copy link
Member Author

billba commented Nov 5, 2017

@aluanhaddad can you expand on that thought?

@ghost
Copy link

ghost commented Nov 6, 2017

@billba Seems like you could replace namespace with class:

a.ts

export class Class<T> {
    constructor(private lambda: (t: T) => void) {}
    twice(): Class<T> {
       return new Class<T>(t => {
           this.lambda(t);
           this.lambda(t);
        });
    }
    static makeTwice<T>(lambda: (t: T) => void): Class<T> {
        return new Class(lambda).twice();
    }
}

export default class Library<T> {
    func(lambda: (t: T) => void): Class<T> {
        return new Class<T>(lambda);
    }
}

b.ts:

import Library from "./a";
const lib = new Library<number>();

lib.func(n => { n.toExponential() });

We"re unlikely to add any new features to namespaces in the future as they are not a JS feature and have been replaced by real ES modules. (See the design goals.)

@billba
Copy link
Member Author

billba commented Nov 6, 2017

That"s interesting. It"s not a complete solution, because sometimes the developer will need access to the interfaces and/or fundamental classes, which would still require type decorations. But all the helper functions could be piled into Library, which would probably cover most of the common use cases. I"ll ponder that.

Thanks for the info about namespaces.

@billba
Copy link
Member Author

billba commented Nov 6, 2017

The slightly annoying thing about the "put the helper functions in a generic class" approach is that the generated JS includes an unnecessary object creation. I hate it when using TypeScript introduces unnecessary JS code just to make typing work.

But it does work.

@aluanhaddad
Copy link
Contributor

@billba I meant that allowing a namespace to be generic is a special case of allowing a variable to be generic.
Consider the following which is currently not possible.

declare const Promise: Promise<T>

@billba
Copy link
Member Author

billba commented Nov 7, 2017

Yeah, this is the heart of the matter. Ultimately what I"m looking for here, and I don"t think I"m alone, is a way to "clamp" a generic class or function or module to a specific type.

I think this would require a new kind of alias:

a.ts

export class GenericClass <T> { ... }
export function genericFunction <T> { ...}

b.ts

import { GenericClass, genericFunction } from "./a";
export alias StringClass = GenericClass<string>;
export alias stringFunction = genericFunction<string>;

c.ts

import { StringClass, stringFunction } from "./b";
const stringClass = new StringClass(...);
stringFunction(...);

These are essentially macros. They are immutable, and every time TypeScript sees the alias, it substitutes in its value. If you mouse over StringClass you see GenericClass<String>. A given alias would be correct syntax if and only if is value would also be correct syntax. Like type aliases they are importable and exportable.

The nice thing about the alias approach is that it doesn"t generate any JavaScript.

@ghost
Copy link

ghost commented Nov 7, 2017

For a class you can do e.g. class StringArray extends Array<string> {}. I don"t think there"s an equivalent for a function that doesn"t involve redeclaring the type.

@aluanhaddad
Copy link
Contributor

With functions you can create a delegating function that fixes the type parameter. With type-inference it"s actually rather terse.

export const stringFunction = (x: string) => genericFunction(x);

Of course this creates an extra value but if we"re talking about extending classes it"s reasonable.

@billba
Copy link
Member Author

billba commented Nov 7, 2017

Right but this is all a lot of unnecessary JS to get around a simple typing operation.

@billba
Copy link
Member Author

billba commented Nov 7, 2017

Basically if you"re working in JS then you can just import the classes and functions from my library and you"re done. Intuitively, using TypeScript for the same purpose shouldn"t be harder or heavier weight than adding a type declaration.

@aluanhaddad
Copy link
Contributor

Actually, this would be covered by #17574

@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2017

Closing in favor of #17574

@mhegazy mhegazy added the Duplicate An existing issue was already created label Nov 14, 2017
@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
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

4 participants