Skip to content

Allow mixin constructor have parameters before rest parameter #37143

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

Open
5 tasks done
HitalloExiled opened this issue Mar 1, 2020 · 3 comments
Open
5 tasks done

Allow mixin constructor have parameters before rest parameter #37143

HitalloExiled opened this issue Mar 1, 2020 · 3 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@HitalloExiled
Copy link

HitalloExiled commented Mar 1, 2020

Search Terms

mixin, constructor parameters

Suggestion

Allow mixin constructor have parameters before rest parameter and infer the combination between mixin parameters.

Use Cases

The following js code is completely valid and functional. However with the actual mixin constructor constraints we can't reproduce this in Typescript.

const mixin1 = (base) => class Mixin1 extends base {
    constructor(m1v1, m1v2, ...args) {
        super(...args);
        this.m1v1 = m1v1;
        this.m1v2 = m1v2;
    }
};
const mixin2 = (base) => class Mixin2 extends base {
    constructor(m2v1, m2v2, ...args) {
        super(...args);
        this.m2v1 = m2v1;
        this.m2v2 = m2v2;
    }
};
class Base {
    constructor(v1, v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
}
class C1 extends mixin1(Base) {
}
class C2 extends mixin2(Base) {
}
class C3 extends mixin2(mixin1(Base)) {
}
console.log(new C1("m1v1", true, 1, {}));
console.log(new C2(false, 2, 1, {}));
console.log(new C3(false, 2, "m1v1", true, 1, {}));

Examples

Same code expected work in ts with constructor inference.

type Constructor = new (...args: any[]) => object;

const mixin1 = <T extends Constructor>(base: T) =>
    class Mixin1 extends base {
        m1v1: string;
        m1v2: boolean;

        constructor(m1v1: string, m1v2: boolean, ...args: any[]) {
            super(...args);

            this.m1v1 = m1v1;
            this.m1v2 = m1v2;
        }
    }

const mixin2 = <T extends Constructor>(base: T) =>
    class Mixin2 extends base {
        m2v1: boolean;
        m2v2: number;

        constructor(m2v1: boolean, m2v2: number, ...args: any[]) {
            super(...args);

            this.m2v1 = m2v1;
            this.m2v2 = m2v2;
        }
    }

class Base {
    v1: number;
    v2: object;

    constructor(v1: number, v2: object) {
        this.v1 = v1;
        this.v2 = v2;
    }
}

class C1 extends mixin1(Base) {
}

class C2 extends mixin2(Base) {
}

class C3 extends mixin2(mixin1(Base)) {
}

console.log(new C1("m1v1", true, 1, { })); // infer mixin1 and Base parameters: [string, boolean, number, object]
console.log(new C2(false, 2, 1, { })); // infer mixin2 and Base parameters: [boolean, number, number, object]
console.log(new C3(false, 2, "m1v1", true, 1, { })); // infer mixin2, mixin1 and Base parameters: [boolean, number, string, boolean, number, object]

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Mar 10, 2020
@wgebczyk
Copy link

wgebczyk commented May 1, 2020

EDITED: Maybe more explanations.

Let's consider the following set of classes as below 'Code Example 1' section.

There are 3 holder classes that keeps string, number and Map.
They work smoothly and as we can see derived class ctor pass subset or arguments OR same options object as each derived class is super set of base members.

If we would like to have object that would keep name and tags, then we have to implement from scratch like in 'Code Example 2' section.

This would be nice to use mixins for such ad hoc compositions. Lets have INeedCount and INeedTags as mixins and compose them like in 'Code Example 3' section.

Easily extending existing classes with various constructors with multiple arguments or options using mixins, would be great!

/////////////////
// Code Example 1

class INeedName {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class INeedCount extends INeedName {
  count: number;
  constructor(name: string, count: number) {
    super(name);
    this.count = count;
  }
}

class INeedTags extends INeedCount {
  tags: Map<string, string>;
  constructor(name: string, count: number, tags: [string, string][]) {
    super(name, count);
    this.tags = new Map(tags);
  }
}

interface INeedNameOptions {
  readonly name: string;
}
class INeedName2 {
  name: string;
  constructor(options: INeedNameOptions) {
    this.name = options.name;
  }
}

interface INeedCountOptions extends INeedNameOptions {
  readonly count: number;
}
class INeedCount2 extends INeedName2 {
  count: number;
  constructor(options: INeedCountOptions) {
    super(options);
    this.count = options.count;
  }
}

interface INeedTagsOptions extends INeedCountOptions {
  readonly tags: ReadonlyArray<[string, string]>;
}
class INeedTags2 extends INeedCount2 {
  tags: Map<string, string>;
  constructor(options: INeedTagsOptions) {
    super(options);
    this.tags = new Map(options.tags);
  }
}

const n = new INeedName("a");
const n2 = new INeedName2({ name: "a" });

const c = new INeedCount("c", 3);
const c2 = new INeedCount2({ name: "c", count: 3 });

const t = new INeedTags("t", 3, [ ["x", "x"], ["y", "y"] ]);
const t2 = new INeedTags2({ name: "t", count: 3, tags: [ ["x", "x"], ["y", "y"] ] });

/////////////////
// Code Example 2

class INeedNameAndTags extends INeedName {
  tags: Map<string, string>;
  constructor(name: string, tags: [string, string][]) {
    super(name);
    this.tags = new Map(tags);
  }
}

interface INeedNameAndTagsOptions extends INeedNameOptions {
  readonly tags: ReadonlyArray<[string, string]>;
}
class INeedNameAndTags2 extends INeedName2 {
  tags: Map<string, string>;
  constructor(options: INeedNameAndTagsOptions) {
    super(options);
    this.tags = new Map(options.tags);
  }
}

const nt = new INeedNameAndTags("t", [ ["x", "x"], ["y", "y"] ]);
const nt2 = new INeedNameAndTags2({ name: "t", tags: [ ["x", "x"], ["y", "y"] ] });

/////////////////
// Code Example 3

const mx_nt_Type = INeedTagsMixin(INeedName);
const mx_nct_Type = INeedTagsMixin(INeedCountMixin(INeedName));

const mx_nt = new mx_nt_Type("t", [ ["x", "x"], ["y", "y"] ]);
const mx_nct = new mx_nct_Type("t", 3, [ ["x", "x"], ["y", "y"] ]);

const mx_nt_Type2 = INeedTagsMixin2(INeedName2);
const mx_nct_Type2 = INeedTagsMixin2(INeedCountMixin2(INeedName2));

const mx_nt2 = new mx_nt_Type({ name: "t", tags: [ ["x", "x"], ["y", "y"] ] });
const mx_nct2 = new mx_nct_Type({ name: "t", count: 3, tags: [ ["x", "x"], ["y", "y"] ] });

@HitalloExiled
Copy link
Author

If I understood. For this type of composition to be possible, the mixin should also know the parameters of the base class. Whether it could be implemented through a generic constraint.

const INeedTagsMixin = <T extends new (options: INeedNameAndTagsOptions) => any>(base: T) =>
  class INeedTagsMixin extends base {
    tags: Map<string, string>;
    constructor(options: INeedNameAndTagsOptions) {
      super(options);
      this.tags = new Map(options.tags);
    }
}

It would be an interesting addition.

@wgebczyk
Copy link

wgebczyk commented Jun 1, 2020

Not exactly. I would like to have option to specify more restricted constructor that "...args: any[]". The mixin from your example would have 2 generic arguments. The mixing class constructor would be intersection of required options' properties plus generic TOptions.

Wrapping class with multiple mixins will look like onion, where each layer consume one "& TOptions" and pass rest further. Typically you will skin passed options with args and final ctor will get type that is defined with. I do hope it is rather clear what I mean - otherwise I could provide more extended example.

interface INeedTagsOptions {
  tags: ReadonlyArray<[string, string]>;
}
// I need to define that this mixin requires TOptions ctor arg to match below super call
const INeedTagsMixin = <T extends new (options: TOptions) => any, TOptions>(base: T) =>
  class INeedTagsMixin extends base {
    tags: Map<string, string>; 
    // here we need INeedTagsOptions to init self and whatever type of constructor arg when used
    constructor(options: INeedTagsOptions & TOptions) {
      super(options); // here we only need TOptions to satisfy "base" ctor
      this.tags = new Map(options.tags);
    }
  }

class Class1 {
  readonly x: number;
  constructor(options: { x: number }) {
    this.x = options.x;
  }
}
class Class2 {
  readonly name: string;
  readonly ugly: boolean;
  constructor(options: { name: string, ugliness: number }) {
    this.name = options.name;
    this.ugly = options.ugliness > 4;
  }
}

const T1 = INeedTagsMixin(Class1);
const t1 = new T1({ x: "xx", tags: [["a", "aa"]] });
console.log(t1.tags.get("a"));
console.log(t1.x);

const T2 = INeedTagsMixin(Class2);
const t2 = new T2({ name: "zz", ugliness: 6, tags: [["a", "aa"]] });
console.log(t2.tags.get("a"));
console.log(t2.ugly);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants