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

Merging constants with namespaces #18163

Open
thorn0 opened this issue Aug 30, 2017 · 13 comments
Open

Merging constants with namespaces #18163

thorn0 opened this issue Aug 30, 2017 · 13 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@thorn0
Copy link

thorn0 commented Aug 30, 2017

I wrote this in closed #13536 first, but thought it might get lost there, so filing a new issue.

Merging consts with namespaces really makes sense. For a real-world example, let's take a popular library: TinyMCE, writing proper type definitions for which requires this feature. Namespaces are really convenient in this case because all the classes from the API are attached to the tinymce namespace object or to its subnamespaces. In the API, there are normal classes like tinymce.Editor and so called 'static classes' like tinymce.EditorManager which are just a type (interface) plus an object value of this type attached to the namespace:

declare namespace tinymce {
  // normal class
  class Editor {
    show(): void;
    // ...
  }
  // "static class"
  interface EditorManager {
    activeEditor: Editor;
    // ...
  }
  const EditorManager: EditorManager;
  // ...
}

Nothing unusual so far, but there is a plot twist. The namespace object tinymce itself implements the tinymce.EditorManager interface. It could be easily and beautifully solved by merging a const with the namespace:

declare const tinymce: tinymce.EditorManager;
declare namespace tinymce {
  // ... see the previous snippet
}

But unfortunately this isn't allowed. The error message is: Cannot redeclare block-scoped variable 'tinymce'.

@thorn0
Copy link
Author

thorn0 commented Oct 11, 2017

Just noticed it's been discussed in #18394:

#18163 Merge value namespaces with other values

  • We can't emit the var statement in this case

Two options:

  1. Consider this feature for type definitions (declare ... / *.d.ts) only, not for emitting. That's where it's most useful anyway.
  2. In the non-ambient context, require that the namespace declaration should be located after the constant just like it's implemented now for merging function declarations with namespaces.
  • Does this merge with the variable or the type?
    • The variable: Now the declared type isn't the actual type?

Just like when a function declaration is merged with a namespace.

  • The type: Now you're changing random other types, maybe accidently?

Is it a big deal? When interfaces are being merged, the same thing happens: you're changing random other types.

  • 👎 Demonstrate how to type this library using existing mechanisms

It's possible, of course, but it's uglier and not DRY.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 11, 2017

Is it a big deal? When interfaces are being merged, the same thing happens: you're changing random other types.

that is not true. you are changing the same type. nothing random about it.

@thorn0
Copy link
Author

thorn0 commented Oct 11, 2017

I don't understand what is meant by random. Neither namespaces nor constant declarations create types other than typeof <id>.

declare const tinymce: tinymce.EditorManager;
declare namespace tinymce {
  // ...
}

So what is the problem? The discrepancy between the type annotation and the actual type? I feel like you mean something else.

@thorn0
Copy link
Author

thorn0 commented Oct 11, 2017

Okay, I think I got it. "Does this merge with the variable or the type?" was a question with two alternatives. The answer is "the variable", not "the type". In my snippet above, the type tinymce.EditorManager shouldn't be affected by these declarations.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 11, 2017

Okay, I think I got it. "Does this merge with the variable or the type?" was a question with two alternatives. The answer is "the variable", not "the type". In my snippet above, the type tinymce.EditorManager shouldn't be affected by these declarations.

but that means that tinymce as a value does not have the type tinymce.EditorManager any longer.. in other words typeof tinymce !== tinymce.EditorManager, which is surprising to say the least.

@thorn0
Copy link
Author

thorn0 commented Oct 11, 2017

Can we add some syntax to indicate that the type annotation isn't final?
Something like:

declare const tinymce: tinymce.EditorManager & ...;

or

declare const tinymce: tinymce.EditorManager & typeof tinymce;

@thorn0
Copy link
Author

thorn0 commented Oct 16, 2017

Seems like there is already at least one precedent where the type annotation doesn't exactly match the actual type: #18043

@mhegazy
Copy link
Contributor

mhegazy commented Oct 16, 2017

narrowing does not change the type.. it narrows the set of possible values for the type based on control flow. i do not think this is related to the issue at hand.

@thorn0
Copy link
Author

thorn0 commented Oct 16, 2017

It's a bit similar for in both cases we can say that the type from the annotation is substituted with a type assignable to it. Also type guards do the same thing.

@manuth
Copy link
Contributor

manuth commented Jun 25, 2019

So I just stumbled across this issue while refactoring type-declarations for a module called inquirer.

So for inquirer the export is an object of type PromptModule but at the same time also provides classes like, for example inquirer.ui.Prompt.

Personally I'd write such a module like this:

declare namespace inquirer {
    namespace ui {
        class Prompt { }
    }
}
declare var inquirer: inquirer.Inquirer;
export = inquirer;

But TypeScript is still throwing errors.
Do I understand right that there's still no good way to go?

Personally I don't really like the way the declarations of the inquirer-module are written currently.
https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/inquirer

Cause of TypeScript's lack of merging-support classes are written as interfaces with new-members and all classes and interfaces are hosted in one single file.

@krisutofu
Copy link

Having the same problems with x_ite. (Javascript implementation of the 3D graphics markup language.)
Type definitions .d.ts were added later but not working. Everything is exported as default X3D.

The javascript API idea is this:

export = X3D;
declare const X3D: X3D;
declare interface X3D
{
  (): Promise<void>;
  noConflict(): X3D;
  getBrowser(): X3DBrowser;
  
}
declare namespace X3D
{
  class X3DBrowser
  {  }
  
}

I had to make it work for me by fixing various errors and problems in the definitions. But member types in the interface instance cannot be used for type expressions. With a namespace instead, I am hitting the same redeclared error.

Multiple ways could have worked but none of these are supported, right?

  1. type definitions in interfaces
  2. use a namespace instead and replace the interface with the type of a subset of the namespace (intersection namespace)
  3. use a const variable with intersection type instead of a namespace. (Type access doesn't work, however interface member types can be accessed in type expression, but with bracket notation.)

A working technique is the duplication of interface code into a namespace (replacing the interface instance). This doubles the maintenance effort.

Now after hours of thinking, I noticed that in this special case a single namespace (plus a function) is sufficient to describe X3D (which is similar to a singleton function). The original interface X3D in .d.ts contains all the defined types as members I think. This interface is equivalent to the type of a namespace that wraps all module definitions. (Ignoring additional interface and type definitions.)

export = X3D;
type X3D = typeof X3D;
declare function X3D(): Promise<void>;
declare namespace X3D
{
  // interface functions
  function noConflict(): X3D;
  function getBrowser(): X3DBrowser;
  
  class X3DBrowser 
}

If the exported type X3D needed to be different from the namespace X3D, it would not work without duplicating code for both.

@miguel-leon
Copy link

I'd like to +1 the capacity to merge namespaces with constants, specifically with arrow functions.
It's a special pain when the arrow function is created somewhere else and can't therefore be declared as a function instead.

I use an ugly workaround in these cases. Sharing it, in case anyone needs it.

declare function FunctionWithNamespace(..._: Parameters<ReturnType<typeof create_arrow_fun>>): ReturnType<ReturnType<typeof create_arrow_fun>>;

(FunctionWithNamespace as any) = create_arrow_fun();
namespace FunctionWithNamespace {
  ...
}

Put the constant as a function with declare (the parameters and return type are the ugliest part), and then assign to the identifier the actual arrow function. It works by taking advantage of the code typescript generates with a var for the namespace, even if the value is assigned before, vars are hoisted so it's ok.

playground

@miguel-leon
Copy link

But then if create_arrow_fun() returns something with overloads, you're out of luck.

Looks like 7 years ago there were discussions of how should the declarations influence the type or the variable.
Today, merging a namespace with a function modify each other: the type gains a callable signature and the variable gains some properties.
It's like merging a class with static properties and a namespace, and then removing the constructor signature.
Feels pretty straightforward to me, like turning a switch on.

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

6 participants