Typescript: Non-exported classes in type declaration files leaking value names

Created on 30 Jun 2019  ·  3Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 3.5.2

Search Terms: ambient module declaration export declare class type name

Code

In a file called classes.d.ts:

declare class A {}
export declare class B extends A {}

In a file called test.js:

let a = require('classes').A;

In a file called test.ts:

import { A } from './types/classes';

_[NEW]_ Assumptions:
The following is a list of assumptions I had when originally opening this issue:

  1. Type Declaration Files work like modules: once you use import or export [on a top-level declaration], only explicitly exported declarations are visible externally.
  2. Type names declared in a Declaration File are always accessible via import types (at least those that are _used_ by exported types.

    • E.g. export class B extends A exports the type names A and B, even if A was not directly exported.

  3. Value names declared in a module-style Declaration File (see #1 above) are only accessible if explicitly exported.

These assumptions are the result of reading the documentation and working with declaration files. Note that the documentation _does not_ mention:

  1. _All_ declarations in a declaration file are implicitly exported.
  2. Special [and undocumented?] export {}; syntax causes only explicitly exported declarations to be available by consumers of the declaration file.

I list them here to provide context for the Expected Behavior section.

Expected behavior:
In both cases , an Error that name 'A' could be found in module 'classes'.

I expect in this case that TypeScript is capable of resolving the following from the declaration file:

  1. The Value "B".
  2. The Types "A" and "B".

In other words, I should be able to use import types to resolve class A, but attempts to _use_ them should fail.

Actual behavior:
No compiler error in either case. TypeScript-powered IDEs (e.g. VSCode) happily show that the _full_ non-exported class A is available.

In short, a non-exported, declared class should resolve in the same way as an interface.

As things stand today, TypeScript erroneously resolves the Value "A".

Playground Link: NA

Related Issues: NA

Docs

Most helpful comment

In a declaration file everything is implicitly exported.

Is that documented anywhere? That should _really_ be documented.

You can prevent that by adding export{};.

This should also be documented. Does this syntax have a name?

With that change you can also no longer use A as type. To fix that you need to export it as interface.

@ajafff What do you mean that you can "export it as interface"?

I see three options:

  1. Convert the base class into an interface.
  2. Rename the A class to AClass and export a type: export type A = AClass;.
  3. Rename the A class to AClass and export an interface: export interface A extends AClass {}.

Here are the issues with those options:

  1. Converting to a class is undesirable as it requires adding all properties to classes that implement the type. For a base class with 24 documented properties, this is untenable.
  2. While using a type alias results in IntelliSense autocomplete [in VSCode] correctly showing the type as a class, the type shown on _hover_ reads as the resolved AClass. Which is _not_ the desirable information to convey.
  3. IntelliSense autocomplete [in VSCode] shows the type as an interface rather than a class.

In my opinion, the third option is the best of the bunch.


Is there really no way to [meaning-full-y] export the type of a class rather than both its type _and_ value?

All 3 comments

Without support for this it is extremely difficult to model a module that has a private base class with many public subclasses.

The following is a snippet of JavaScript:

//                           ↓ OK!
/** @type {import('classes').A} */
let x;

//                                      ↓ ERROR!
let y = x instanceof require("classes").A;

The import type currently works as expected. 👍
The instanceof line beneath it does _not_ report the expected error. 👎

In a declaration file everything is implicitly exported. You can prevent that by adding export{};.
With that change you can also no longer use A as type. To fix that you need to export it as interface.

In a declaration file everything is implicitly exported.

Is that documented anywhere? That should _really_ be documented.

You can prevent that by adding export{};.

This should also be documented. Does this syntax have a name?

With that change you can also no longer use A as type. To fix that you need to export it as interface.

@ajafff What do you mean that you can "export it as interface"?

I see three options:

  1. Convert the base class into an interface.
  2. Rename the A class to AClass and export a type: export type A = AClass;.
  3. Rename the A class to AClass and export an interface: export interface A extends AClass {}.

Here are the issues with those options:

  1. Converting to a class is undesirable as it requires adding all properties to classes that implement the type. For a base class with 24 documented properties, this is untenable.
  2. While using a type alias results in IntelliSense autocomplete [in VSCode] correctly showing the type as a class, the type shown on _hover_ reads as the resolved AClass. Which is _not_ the desirable information to convey.
  3. IntelliSense autocomplete [in VSCode] shows the type as an interface rather than a class.

In my opinion, the third option is the best of the bunch.


Is there really no way to [meaning-full-y] export the type of a class rather than both its type _and_ value?

Was this page helpful?
0 / 5 - 0 ratings