Relax visibility rules for type-aliases when 'declaration' compiler option is set.
See original GitHub issueOne common thing I use type-aliases for is to give shorter, more expressive names to commonly used types throughout a file.
This is especially useful for callback signatures that see common use throughout a module, as well as string-literal types used in a union type, like type HttpMethods = 'get' | 'post' | 'put' | 'delete'
.
Let’s look at the following simplified example:
type CharCallback = (c: string, i: number) => string;
export function mapOnChar(str: string, fn: CharCallback): string {
const newStr = [];
for (let i = 0, lim = str.length; i < lim; i++)
newStr.push(fn(str.charAt(i), i));
return newStr.join('');
}
When using the declaration
compiler option, this example fails to compile with the following error:
error TS4078: Parameter 'fn' of exported function has or is using private name 'CharCallback'.
This initially makes sense; the type-alias is not being exported so it is private. However, if you look at the type-alias, the only thing “private” about it is the name it was given, and an alias’ name is not really important to or needed for a definition file.
There is no reason that the un-exported type-alias in the example cannot be automatically de-aliased back into (c: string, i: number) => string
and that used in its place when the definition file is emitted.
However, if the alias in the example were to be exported, then it should not be de-aliased in the definition file.
I should point out that this suggestion is considering type-aliases only. If the CharCallback
type was re-written as interface CharCallback { (c: string, i: number): string }
, then that would be a good case to raise an error. An interface
is generally considered more “concrete” than a simple type-alias, and so its name should be preserved and used in the definition file.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:5
- Comments:6 (2 by maintainers)
Top GitHub Comments
I see. But a lot of this stems off of just how “real” type-aliases are. Since I started working with TypeScript, I’ve been seeing them being treated as non-corporeal; they vanish as soon as you look at them. This has led me to believe that the community consensus is that this is how a type-alias should be treated by default.
They disappear in the TypeScript playground, they disappear in the IDEs, they’re even known to disappear in compiler error messages. Interfaces, on the other hand, keep their name like it is holy throughout the entire code-base.
The TypeScript Handbook even makes special note of this in its “Interfaces vs Type Aliases” section:
This
declaration
compiler option issue is the only circumstance I’ve found so far where a type-alias’ name is treated in a first-class manner. It seems like an inconsistent application of the concept.If I didn’t make an alias public by exporting its name into my module’s API, then it’s either not important enough to keep its name or I made a mistake when authoring my API (and the only negative consequence would be that my API’s type-definitions are a little messier than I intend, but it would still function fine, otherwise).
I know that perhaps the idea was to help people catch that kind of mistake, but I argue that this violates pre-established rules regarding type-aliases, and it’s more important for the longevity and continued maintenance of the language to keep those rules until we have a really good reason to break them.
And if you really look at it, the trade-off on compromising this rule is actually not all that great. I’m basically being forced to either:
This is just not a good trade-off for the safety and responsibility you’ve suggested we’re gaining with this. With this restraint on type-aliases, there is significant pressure to make something messy for someone somewhere.
Here is the problem, an ambient module has all its members automatically exported. That was a design decision we made early on to avoid clutter in declaration files (turned out not to be so wise after all:)). So for instance:
In the example above, both
x
andfoo
are exported. theexport
keyword is not required, nor needed.Now to the declaration emitter. the emitter is trying to emit
function mapOnChar(str: string, fn: CharCallback): string
, it figures it needsCharCallback
, so it writes that as well; but given the above behavior of ambient modules, this means it is making your typeCharCallback
public, whereas it was not. So options:So we picked the last option, as it seemed the safest, and most responsible.
Now for relaxing the rule for type aliases, not sure why a type alias is different from an interface, different from a class that your users will only use in a type position… A change in any of these will break the compilation of your users regardless if they took a dependency on it.