You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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:
declareconsttinymce: tinymce.EditorManager;declarenamespacetinymce{// ... see the previous snippet}
But unfortunately this isn't allowed. The error message is: Cannot redeclare block-scoped variable 'tinymce'.
Consider this feature for type definitions (declare ... / *.d.ts) only, not for emitting. That's where it's most useful anyway.
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.
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.
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.
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.
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.
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.
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.
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?
type definitions in interfaces
use a namespace instead and replace the interface with the type of a subset of the namespace (intersection namespace)
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.)
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.
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.
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.
Activity
thorn0 commentedon Oct 11, 2017
Just noticed it's been discussed in #18394:
Two options:
declare .../*.d.ts) only, not for emitting. That's where it's most useful anyway.Just like when a function declaration is merged with a namespace.
Is it a big deal? When interfaces are being merged, the same thing happens: you're changing random other types.
It's possible, of course, but it's uglier and not DRY.
mhegazy commentedon Oct 11, 2017
that is not true. you are changing the same type. nothing random about it.
thorn0 commentedon Oct 11, 2017
I don't understand what is meant by random. Neither namespaces nor constant declarations create types other than
typeof <id>.So what is the problem? The discrepancy between the type annotation and the actual type? I feel like you mean something else.
thorn0 commentedon 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.EditorManagershouldn't be affected by these declarations.mhegazy commentedon Oct 11, 2017
but that means that
tinymceas a value does not have the typetinymce.EditorManagerany longer.. in other wordstypeof tinymce !== tinymce.EditorManager, which is surprising to say the least.thorn0 commentedon Oct 11, 2017
Can we add some syntax to indicate that the type annotation isn't final?
Something like:
or
thorn0 commentedon 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 commentedon 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 commentedon 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.
neurolag commentedon Jun 25, 2019
So I just stumbled across this issue while refactoring type-declarations for a module called
inquirer.So for
inquirerthe export is an object of typePromptModulebut at the same time also provides classes like, for exampleinquirer.ui.Prompt.Personally I'd write such a module like this:
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 commentedon Oct 3, 2024
Having the same problems with
x_ite. (Javascript implementation of the 3D graphics markup language.)Type definitions
.d.tswere added later but not working. Everything is exported as defaultX3D.The javascript API idea is this:
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
redeclarederror.Multiple ways could have worked but none of these are supported, right?
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 originalinterface X3Din.d.tscontains 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.)If the exported
type X3Dneeded to be different from thenamespace X3D, it would not work without duplicating code for both.miguel-leon commentedon Oct 20, 2024
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.
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 avarfor the namespace, even if the value is assigned before,vars are hoisted so it's ok.playground
miguel-leon commentedon Oct 25, 2024
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.