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

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

Closed
a-student opened this issue Mar 13, 2019 · 22 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@a-student
Copy link

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 () {
        }
    };
}
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 14, 2019
@RyanCavanaugh
Copy link
Member

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.

@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.

@a-student
Copy link
Author

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
}

@taoqf
Copy link

taoqf commented Apr 3, 2019

I think this is an issue, too.

@ozyman42
Copy link

ozyman42 commented Apr 28, 2019

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

@cdata
Copy link

cdata commented Sep 17, 2019

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 🙏

@trusktr
Copy link
Contributor

trusktr commented Oct 8, 2019

Shouldn't this issue be open?

@trusktr
Copy link
Contributor

trusktr commented Oct 8, 2019

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.

@trusktr
Copy link
Contributor

trusktr commented Oct 8, 2019

If a function foo has a generic param, then the typeof operator could perhaps return a generic type, and we could write:

type FooBar = ReturnType<(typeof foo)<Bar>>

That would be a breaking change to TypeScript though.

@trusktr
Copy link
Contributor

trusktr commented Dec 21, 2019

Please see #35822

@trusktr
Copy link
Contributor

trusktr commented Jan 7, 2020

I re-opened this at #36060

@sebastiandittrich
Copy link

sebastiandittrich commented Aug 9, 2020

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
}

@gumT3KBZ9zMY7kzv
Copy link

gumT3KBZ9zMY7kzv commented May 12, 2021

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;
    }
}

@tchakabam
Copy link

tchakabam commented Aug 11, 2021

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 🙏

@cdata not exactly! :) see the prior workaround the OP pointed to:

Declare the return type explicitly:

In the case of a mixin function (which is exactly the problem I had here too), that means you need to return the union (&) of the 2 constructor-types which you will mixin together. It is helpful to provide therefore an interface decl outside of the mixin function which implements by the class that the mixin function returns.

So, if your mixin class is resulting in a union of interface i.e class A & B , then your mixin function should return something like
(new (...args: any[]) => A) & (new (...args: any[]) => B).

You can come up with various ways to shorten this notation using generic type templates. I can post some helper code and type utilities for this if there is interest.

I agree this isn't a bug per-se. The type-declaration just expects your mixin function to return types explicitely that it actually can express i.e export in a d.ts, and in this case that will mean explicitely declaring the resulting mixin interface outside of the mixin function so that it can be used as a return type of it.

EDIT and yes, that is in fact the previous answer here explained in words :D

@Hobart2967
Copy link

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 :)

@Julien-R44
Copy link

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;
  }
}

@chlbri
Copy link

chlbri commented Nov 18, 2022

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.

@leppaott
Copy link

leppaott commented Feb 2, 2023

Same thing with "constant class".

export const testClass = class {
  private field = {}
}

Property 'field' of exported class expression may not be private or protected.ts(4094)

Interestingly # private class fields indeed work fine.

Using workaround:

class TestClass {}
export const testClass = new TestClass();

@Marckon
Copy link

Marckon commented Apr 11, 2023

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.

@zyf0330
Copy link

zyf0330 commented Jun 2, 2023

you can write like this

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

export function test2 (): typeof C  {
    return class extends C {};
}

@ThePlenkov
Copy link

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.

it would be great if ESlint rule will be already aware. I had some error and quick fix was generation of private _data + get/set data methods. And it was throwing errors. Replacing with # helped to solve it.

@ThePlenkov
Copy link

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

rajsite added a commit to ni/nimble that referenced this issue Oct 5, 2023
…mns (#1587)

# Pull Request

## 🤨 Rationale

Resolves #1211 

In Angular, we didn't have a solution for the code duplication that came
with multiple columns being fractional width and groupable. This PR
creates mixins for fractional width columns and groupable columns to
reduce the amount of duplication. There is still the potential for
future code reduction within the tests, but I decided that coming up to
a solution single-sourcing more of our test code was out of scope for
this PR.

## 👩‍💻 Implementation

- Create `mixinFractionalWidthColumnAPI` function that allows a
`NimbleTableColumnBaseDirective` to claim support for being fractional
width (i.e. inputs for `fractionalWidth` and `minPixelWidth`). This
function is exported from the existing `@ni/nimble-angular/table-column`
entry point.
- Create `mixinGroupableColumnAPI` function that allows a
`NimbleTableColumnBaseDirective` to claim support for being groupable
(i.e. inputs for `groupIndex` and `groupingDisabled`). This function is
exported from the existing `@ni/nimble-angular/table-column` entry
point.
- Update existing table columns to use these new mixins
- These changes required making `renderer` and `elementRef` public
(though marked as internal) in `NimbleTableColumnBaseDirective` because
otherwise TS error 4094 is triggered. This is related to the
`mixinFractionalWidthColumnAPI` and `mixinGroupableColumnAPI` functions
having an implicit return type and that inferred type including
`NimbleTableColumnBaseDirective`. [The scenario I was hitting is
described in this TypeScript
issue](microsoft/TypeScript#30355).

## 🧪 Testing

- Verified existing unit tests are passing
- Verified Angular example app still works as expected

## ✅ Checklist

<!--- Review the list and put an x in the boxes that apply or ~~strike
through~~ around items that don't (along with an explanation). -->

- [ ] I have updated the project documentation to reflect my changes or
determined no changes are needed.

---------

Co-authored-by: Milan Raj <rajsite@users.noreply.github.com>
Sanae6 added a commit to roobscoob/nnfileabstraction that referenced this issue Nov 18, 2023
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