question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Relax visibility rules for type-aliases when 'declaration' compiler option is set.

See original GitHub issue

One 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:open
  • Created 7 years ago
  • Reactions:5
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

4reactions
JHawkleycommented, Feb 25, 2017

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:

One difference is that interfaces create a new name that is used everywhere. Type aliases don’t create a new name — for instance, error messages won’t use the alias name.

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:

  1. Unnecessarily export simple types into my public API, making the API generally messier, but keeping my own code cleaner.
  2. In-line types that have very common use in my code, making messier and more difficult to maintain code, but cleaner APIs.

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.

1reaction
mhegazycommented, Feb 24, 2017

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:

declare module "Mod" {
    var x: number;
    export function f(): void;
}

In the example above, both x and foo are exported. the export 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 needs CharCallback, so it writes that as well; but given the above behavior of ambient modules, this means it is making your type CharCallback public, whereas it was not. So options:

  • Do not mind, make it public any ways
    • Pros: no error reported
    • Cons: your API surface area has changed in a way you did not anticipate, moreover, you have no way of really “hiding” a declaration. Your users can now take a dependency on this type/value, and you can break them without noticing
  • Try to inline the type
    • Pros: again, no error reported
    • Cons: serializing types is not that simple, there are cases where that is not possible, e.g. classes, self-referencing types, etc… and will make your .d.ts file look less organized, than your .ts file
  • Let the user know, and they can decide
    • Pros: either make it public, and commit to supporting it, or inline it, or not expose it at all
    • Cons: well… an error is reported

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

C++ Dialect Options (Using the GNU Compiler Collection ...
The One Definition Rule is relaxed for types without explicit visibility specifications that are defined in more than one shared object: those declarations...
Read more >
TSConfig Reference - Docs on every TSConfig option
Intro to the TSConfig Reference. A TSConfig file in a directory indicates that the directory is the root of a TypeScript or JavaScript...
Read more >
gcc(1) - Linux manual page - man7.org
Some options control the preprocessor and others the compiler itself. ... The One Definition Rule is relaxed for types without explicit visibility ......
Read more >
3.5 Options Controlling C++ Dialect - Lahey Fortran
Using the GNU Compiler Collection (GCC) ... The One Definition Rule is relaxed for types without explicit visibility specifications that are defined in...
Read more >
Overview - TypeScript
bigint to the lib setting in your compiler options. Non-unit types as union discriminants. TypeScript 3.2 makes narrowing easier by relaxing rules for...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found