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.

Generating type definitions for mixin classes with protected members

See original GitHub issue

TypeScript Version: 2.4.2

Code:

I’m using mixins as described by: https://github.com/Microsoft/TypeScript/pull/13743

export type Constructor<T> = new(...args: any[]) => T;
export function Unsubscriber<T extends Constructor<{}>>(Base: T)  {
  class Unsubscriber extends Base implements OnDestroy {
    protected unsubscribe: Subject<void> = new Subject();

    ngOnDestroy() {
      this.unsubscribe.next();
      this.unsubscribe.complete();
    }
  }
  return Unsubscriber;
}

If I compile this code with "declaration": true to get type definitions for my library, I get the following error:

ERROR in (truncated)/mixins.ts (...): Return type of exported function has or is using private name 'Unsubscriber'.

One solution is to add an interface…

export interface IUnsubscriber extends OnDestroy {
  unsubscribe: Subject<void>;
}

…and have my mixin function have a return type of Constructor<IUnsubscriber>. This works, but it forces me to make the properties/methods exposed by my mixin be public even in cases where I want them to be protected.

Short of adding protected members to interfaces (which I’m not sure is the right thing to do), this seems to be a limitation of the currently supported mixin strategy.

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:46
  • Comments:11 (2 by maintainers)

github_iconTop GitHub Comments

18reactions
web-padawancommented, Nov 27, 2019

Here is my research regarding possible workarounds for TS mixins with protected methods.

Overview

These are 3 approaches that can be used for writing mixins that need to expose protected methods. They all allow subclasses extending such mixins to call these methods using super.

1. Symbol approach

Pros

  • Methods are truly protected (can’t be called from outside, e.g. by third party code)

  • Methods are “public” in terms of TS and can be annotated on the interface

  • Explicit exports potentially help with refactoring of the consumers’ subclasses

  • Methods are excluded from the DevTools console’s auto-complete list (good for user)

  • Avoid potential name conflicts if user extends a class with their own methods

Cons

  • Consumers have to import symbols if they need to override protected methods

  • Jump to definition is not ideal (jumps to the place where Symbol() is created)

2. Dumb class approach

Pros

  • No need to import anything (except the mixin itself) for the consumer component

  • Methods are easy to access in the DevTools console (good for maintainer)

Cons

  • Both interface (for public API) and abstract class (for protected API) are needed

  • The abstract class ends up in the JS output (as empty class with no methods)

  • The mixin function needs to confusingly accept the argument with a type cast (instead of the actual expected class) in order to pick up the annotated protected methods from it:

// with dumb class: confusing
<T extends Constructor<SlottedItemsClass>>(base: T)

// without dumb class: clean
<T extends Constructor<LitElement>>(base: T)
  • Need for extra super check: super._focus && super._focus() - this is needed because the abstract classes mark methods using _focus?(), i.e. “possibly undefined”

  • Jump to definition from the subclass goes to a dumb class

3. Public methods

Pros

  • No extra code needed (symbols / dumb classes)

Cons

  • TS users would get protected methods listed in the code completion

  • Messing up public and protected API might need tweaks for tools

Summary

Personally, I like symbols approach more because it’s cleaner and bulletproof. The “jump to definition” inconvenience is a drawback that is more of a personal preference.

On the other hand, the dumb class approach has its benefits: once the issue is resolved, we can get rid of those dumb classes and potentially keep the methods themselves unchanged.


@weswigham according to your comment at https://github.com/microsoft/TypeScript/issues/17293#issuecomment-522491710, what do you have on your mind to tackle these issues? Is there a plan, or does this still need a concrete proposal that you mentioned?

8reactions
octogonzcommented, Mar 1, 2019

BTW I recently came up with an alternate way to represent mixins in TypeScript. Consider this example:

// Helper that copies properties from base's prototype
// to child's prototype
function extend(child: any, base: any): void {
  for (var property in base) {
    var descriptor = Object.getOwnPropertyDescriptor(base, property) || { value: base[property] };
    Object.defineProperty(child, property, descriptor);
  }
}

// The mixin base classes
class Base1 {
    constructor() {
        console.log('Base1.constructor() called')
    }
    public method1(): void {
        console.log('method1() called')
    }
}

class Base2 {
    constructor() {
        console.log('Base2.constructor() called')
    }
    public method2(): void {
        console.log('method2() called')
    }
}

class Base3 {
    constructor() {
        console.log('Base3.constructor() called')
    }
    private _prop3 = 123;
    public get prop3(): number {
        return this._prop3;
    }
}

// Collect the base classes into an intermediary "Mixin" class
type Mixin = Base1 & Base2 & Base3;
const Mixin: { new(): Mixin } = class { 
    constructor() {
        Base1.call(this);
        Base2.call(this);
        Base3.call(this);
    }
} as any;
extend(Mixin.prototype, Base1.prototype);
extend(Mixin.prototype, Base2.prototype);
extend(Mixin.prototype, Base3.prototype);

// Extend from the "Mixin" class
class Child extends Mixin {
    public method4(): void {
        console.log('method4() called')
    }    
}

const child = new Child();

child.method1();
child.method2();
console.log('prop3 = ' + child.prop3);
child.method4();

The behavior is probably unsound if the base classes happen to have members with the same name. But this issue could be detected in the extend() function.

There are some interesting advantages:

  • The base classes don’t need to be represented specially; they are ordinary TypeScript classes
  • You can use private members without any trouble
  • The generated .d.ts file is simple and understandable

Not sure I’d recommend to do this in real code, but it’s interesting to contrast with the class-expression approach to mixins.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Documentation - Mixins - TypeScript
The pattern relies on using generics with class inheritance to extend a base class. TypeScript's best mixin support is done via the class...
Read more >
Typescript mixin with protected access - Stack Overflow
A mixin called Activatable that needs access to the protected members of User . type Constructor<T = {}> = new (... args: any[])...
Read more >
The Mixin Pattern In TypeScript – All You Need To Know Part 2
Mixin class methods then can refer to the generic type as this[ 'Element' ] . The generic arguments of the outer and nested...
Read more >
How to Handle Multiple Inheritance and TypeScript Mixins
Multiple inheritance is a feature where a class can extend more than one base class. The problem. What if we want to create...
Read more >
Mixin Classes in TypeScript - Marius Schulz
A mixin class is a class that implements a distinct aspect of functionality. Other classes can then include the mixin and access its...
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