TypeScript: False error TS4094: ... exported class expression may not be private or protected.

TypeScript Version: 3.3.3333

Code

src/test.ts:

export function test () {
    return class {
        private privateMember () {
        }
    };
}

tsconfig.json:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "declaration": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

Expected behavior: No compilation error.

Actual behavior: Compiler prints error: $ tsc src/test.ts(1,17): error TS4094: Property ‘privateMember’ of exported class expression may not be private or protected.

Playground Link: not possible to provide.

Workaround

Declare the return type explicitly:

export function test (): new() => Object {
    return class {
        private privateMember () {
        }
    };
}

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 26
  • Comments: 22 (1 by maintainers)

Commits related to this issue

Most upvoted comments

The error message didn’t accidently write itself… exported anonymous classes can’t have private or protected members if declaration emit is enabled, because there’s no way to represent that in a .d.ts file.

Perhaps we should extend d.ts to handle this case then. Ideally the declarations file and the ts source file would be 100% compatible.

Shouldn’t this issue be open?

It is currently not possible to generate declaration files when using a class mixin pattern, even if that pattern would otherwise compile and type-check just fine. It would be nice to have support for this 🙏

here is how you can fix it, instead of

function makeClass() {
    return class {
         private x = 0;
         public y = 1;
    }
}

do

type PublicConstructor<T> = new () => T;
interface MyPublicInterface {
    y: number;
}
function makeClass(): PublicConstructor<MyPublicInterface> {
    return class {
         private x = 0;
         public y = 1;
    }
}

Interesting fix:

Moving the mixin to its own file and doing a default export works:

type Constructor<T = {}> = new (...args: any[]) => T

export default <T extends Constructor>(base: T) => class SomeClass extends base {
    protected something: boolean = false
}

While a named export throws the error:

type Constructor<T = {}> = new (...args: any[]) => T

export const mixin = <T extends Constructor>(base: T) => class SomeClass extends base {
    protected something: boolean = false
}

It is possible to use private in declaration files. In my project, for example, I found a lot of them in node_modules folder: tslint/lib/formatters/checkstyleFormatter.d.ts:

export declare class Formatter extends AbstractFormatter {
    static metadata: IFormatterMetadata;
    format(failures: RuleFailure[]): string;
    private escapeXml; <- HERE
}

Another solution can be to just use the ES2020 private fields if you ever encounter this problem. This will avoid you to define a return type, which can be annoying if your mixin is quite complex :

function makeClass() {
  return class {
       #x = 0;
       public y = 1;
  }
}

Solution: Just give the function a proper return type declaration. This one then needs to be an indirect connection, e.g. an interface describing the prototype of the returned class. Then, the compiler stops complaining 😃

In particular, it’d be nice to be able to do something like

function foo<T>(arg: T): SomeType<T> { ... }

type Bar = {num: number}

type FooBar = ReturnType<typeof foo<Bar>>

I get what you’re saying, that foo<Bar> is already a type, so typeof doesn’t make sense. But I think you get what’s missing.

This is valid:

type FooBar = ReturnType<typeof foo>

But in that example, TypeScript sets the type of T to unknown, so the type of FooBar is SomeType<T>.

Basically, it just intuitively seems like there should be some way to pass a generic arg there, but we can’t, and TypeScript automatically sticks unknown into it. Example on playground shows automatic unknown type for T.

I think this is an issue, too.

Interesting fix:

Moving the mixin to its own file and doing a default export works:

type Constructor<T = {}> = new (...args: any[]) => T

export default <T extends Constructor>(base: T) => class SomeClass extends base {
    protected something: boolean = false
}

While a named export throws the error:

type Constructor<T = {}> = new (...args: any[]) => T

export const mixin = <T extends Constructor>(base: T) => class SomeClass extends base {
    protected something: boolean = false
}

isn’t it a bug then? So it’s doing exactly same, but with two files instead of one. I’m quite surprised to see that this issue is Closed while indeed it must not and should be fixed

Although use #to declare private member can avoid this declaration error, it will make the property not readable, plus it can not replace protected. I only want some method or property not overridable in some cases.

Just play with Omit type 😎. Just a example with vitest :

import { vi as _vi } from 'vitest';

type VitestUtils = Omit<
  typeof _vi,
  '_timers' | '_mockedDate' | '_mocker' | 'getImporter' | '_config' // never also works fine
>;
type Primitive = number | boolean | string;
type Props = {
  [key in string]?:
    | Primitive
    | Primitive[]
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    | ((this: VitestUtils, ...args: any) => any);
};

export default function extendVitest<T extends Props>(options: T) {
  return Object.assign(_vi as VitestUtils, options);
}

NB: If you don’t know the private members of the class, just put never as second parameter of Omit.