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

Cannot use class as type if nested into object or class #46938

Closed
Danielku15 opened this issue Nov 28, 2021 · 4 comments
Closed

Cannot use class as type if nested into object or class #46938

Danielku15 opened this issue Nov 28, 2021 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Danielku15
Copy link

Bug Report

πŸ”Ž Search Terms

nested class type object

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Type System Behavior

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

class Test {}
const Const = {
    Test: Test,
    Test2: class {

    }
}
class Class {
    static Test = Test;
    static Test2 = class {
    }
}
declare const ConstAsInTsDef: { // d.ts files will look like this for the const Const = {} variant above
    Test: typeof Test
}

const a: Const.Test = new Const.Test();
const b: Const.Test2 = new Const.Test2();
const c: Class.Test = new Class.Test();
const d: Class.Test2 = new Class.Test2();
const e: ConstAsInTsDef.Test = new ConstAsInTsDef.Test(); 

πŸ™ Actual behavior

TypeScript does not allow to use the nested types within objects or classes as part of type annotations. It complains with errors like Cannot find namespace 'Const'.ts(2503) as TypeScript only seem to allow referencing nested types only if they are nested using namespaces

πŸ™‚ Expected behavior

We should be able to use also classes/types aliased through objects or nested classes as type annotations in variables, return types etc.

Use Case

This topic relates a bit to #4529

I am bundling up my library into an all-in-one module (using Rollup) containing one entry point to my library while I want to keep a bit of a module-alike organization of my classes through this nesting approach. Considering all the current restrictions regarding exports and namespaces going for nesting is currently the "best" approach with keeping 1 file for your library.

Consumers of my library can access all the classes to create instances but they cannot use my types in their codebases to annotate their variables and functions.

My library is available in a wide variety of environments supporting module (ESM, UMD) and non-module (classical browser script include) usages.

@MartinJohns
Copy link
Contributor

MartinJohns commented Nov 28, 2021

I don't see a bug here.


The correct syntax for what you want is: InstanceType<(typeof Const)['Test']>

  • Const refers to a value, but you want the type, so you use typeof Const.
  • There's no dot-notation syntax to access the members of a type, only the bracket notation, so you write ['Test'].
  • And lastly this gives you the constructor, but you want the instance type, so you use InstanceType<..>.
  • In the case of the class the Class refers to an instance, but as you try to access a static member you need to refer to the prototype, so you write typeof Class.

This is all working as intended.

@Danielku15
Copy link
Author

Writing InstanceType<(typeof Const)['Test']> sounds a bit very verbose and unintuitive. As indicated on the usecase, the goal is to re-export types from a library into namespaces for usage by others. Expecting everyone to write a syntax like InstanceType<(typeof Const)['Test']> every time they want to describe the return type from the library is not realistic.

I will try to give a bit more insight why I consider it as a misbehavior and what my actual workflow is. Hopefully this changes our discussion in a way that will allow me filing the right kind of issue with the right details πŸ˜‰

This is the setup and files I get from the individual steps in my compilation chain:

// Test.ts
export class Test { }
// MyLib.ts (and MyLib.js after TS compilation)
import { Test } from './test';
export const Const = {
    Test: Test
}
// MyLib.js after rollup (UMD)
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.myLib = {}));
}(this, (function (exports) { 'use strict';
  class Test {}
  const Const = {
    Test
  };
  exports.Const= Const;
  Object.defineProperty(exports, '__esModule', { value: true });
})));
// MyLib.mjd after rollup (ESM)
class Test {}
const Const = {
  Test 
};
export { Const }
// MyLib.d.ts
declare class Test { }
declare const Const {
  Test: typeof Test
}
export { Const };

This output leads to the reported issue where Const.Test cannot be referred to in type annotations/hints.
When it comes to the usage during runtime everything is fine. You can access the "class" with the dotted notation.

import { Const } from 'myLib';
const a: Const.Test = new Const.Test(); // error on the type annotation, fine on the constructor

There is a workaround to this "problem" I am currently evaluating. This structure rather builds on top of the module concepts. It will ultimately result in namespaces in the d.ts which allows correct usage of the class.

// Const.ts
export { Test } from './Test'
// MyLib.ts and MyLib.js after TS and before Rollup
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.myLib = {}));
})(this, (function (exports) { 'use strict';
  export * as Const from './Const.ts'
  // MyLib.js (after rollup)
  class Test { }
  var index$1 = Object.freeze({
    __proto__: null,
    Test: Test
  });
  exports.Const = index$1;
  Object.defineProperty(exports, '__esModule', { value: true });
}));
// MyLib.mjs
class Test {}
var index$1 = Object.freeze({
  __proto__: null,
  Test: Test
});
export { index$1 as Const };
// MyLib.d.ts (through Rollup) 
declare class Test { }
type index_d$1_Test = Test;
declare const index_d$1_Test: typeof Test;
declare namespace index_d$1 {
  export {
    index_d$1_Test as Test,
  };
}
export { index_d$1 as Const };

As you can see the actual JS output is structurally the quite same to the other variant. But the big difference lies in the d.ts which now has a namespace and allows usage of Const in type annotations and also the constructor is allowed. And TypeScript seems to consider this d.ts as valid.

As indicated in #4529 putting exports into a namespace (like in the generated d.ts) leads to a TS1194: Export declarations are not permitted in a namespace.

Maybe we could say it's a bug (or missing feature if you prefer 😁 ) that we have TS1194 with the following input (?):

// close to the structure above
namespace Const {
    export { Test } from './test';
}
export { Const };
// or even shorter
export namespace Const {
  export { Test } from './test';
}

My workaround to define my namespaces/scopes as modules and use the re-exporting of modules seems to give me the output I like. So I could live with this. It feels inconvenient that I cannot do the same thing with keeping all library exports within one file requiring me to split it up.

What's your opinion on this use case?

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Nov 29, 2021
@RyanCavanaugh
Copy link
Member

The syntax you're requesting to do something already has a different meaning:

const Const = {
    Test2: class {
        m = 10;
    }
}
namespace Const {
    export interface Test2 {
        m: string;
    }
}
const m: Const.Test2 = { m: "hello" }

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants