microsoft / microsoft/TypeScript

Constructor generic types and `this` parameter (TS1092, TS2681)

Offen
#40,451 9 Kommentare 37 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

In Discussion Suggestion
Vorherrschende Sprache
Go
Sterne
111k
Forks
14.3k
Ø Merge
2 T. 4 Std.
Gemergte PRs (30 T.)
132

Beschreibung

Search Terms

constructor
this
parameter
type

Suggestion

Allow constructors to specify a this parameter, and allow generic type parameters on constructors.

  • Remove TS1092: Type parameters cannot appear on a constructor declaration.
  • Remove TS2681: A constructor cannot have a 'this' parameter.

Use Cases

Subclass Type Inference

The primary reason for this change would be to allow constructors to take in parameters that are in some way related to the this type of a subclass. Specifically, it provides a solution for the commonly requested pattern of initializing classes with all of their required properties:

class Base {
    foo!: boolean;
    constructor<T extends Base>(this: T, data: T) { // or data: Partial<T>, etc
        Object.assign(this, data);
    }
}
class A extends Base {
    bar: number = 0;
}
class B extends Base {
    baz: string = 'hello';
}
new A({foo: true, bar: 123});
new B({foo: false, baz: 'world'});

The huge advantage of this pattern is that the constructor does not have to be individually specified for every single subclass to maintain type safe initialization. In use cases where a class has many subclasses (e.g. various message types of a protocol that all share some common properties in the base class), the choice is between having manually typed constructors for each subclass and accepting the any type to be used with Object.assign, which opens up various potential problems.

Most other use cases are directly relevant in this example.

Single-Use Generics

Class methods use generics locally for situations where the inputs and outputs of the method are independent of the class itself.

While this might be less common with constructors, there are situations where generics are only necessary during initialization and become redundant afterwards. In these situations the types are not inherently tied to the overall class, so they should ideally be defined separately.

class Foo {
    constructor(foo: Foo, options: FooOptions);
    constructor<T extends Bar>(bar: T, options: BarOptions<T>); // make sure BarOptions are specific to T!*
    constructor(fooOrBar: Foo | Bar, options: FooOptions | BarOptions<any>) { // don't care at runtime anyway
        if (fooOrBar instanceof Foo) {
            this.applyFooOptions(options);
        } else {
            this.applyBarOptions(options);
        }
    }

When referenced elsewhere in the codebase the class can then be free of generics clutter which was only necessary during initialization.

Reach-Through Self-Referencing Generics

Self-referencing generics (not sure this is the correct name) can be useful for obtaining information about a subclass, but they are only applicable if there is one layer of subclassing:

class Base<T extends Base<any>> {
    constructor(data: T) {
        Object.assign(this, data);
    }
}

class Foo extends Base<Foo> {}
new Foo({});

class Bar extends Base<Bar> {
    barInfo: string;
}
new Bar({barInfo: "test"});

// Bar is a standalone class so it must set the generic, but it also has a subclass that adds some extra info
class BarAndMore extends Bar { // bar has no generics, can't change Base<Bar> constructor parameter ?
    barExtendedInfo: string;
}
new BarAndMore({
    barInfo: "test",
    barExtendedInfo: "more" // ERROR: Object literal may only specify known properties, and 'barExtendedInfo' does not exist in type 'Bar'.
});

With constructor generics the following would become possible, even without this parameters:

new BarAndMore<BarAndMore>({...barAndMore});

And when the this parameter is used, it becomes a way to reach directly to the furthest subclass, which self-referencing generics prevent. This is of course already available in ordinary methods.

Decorators
class Base {
    constructor<T>(this: {id: T}, id: T) { this.id = id; }
}
class A {
    @IsNumber() id: number;
}
class B {
    @IsString() id: string;
}
new A(123);
new B(123); // ERROR: Argument of type 'number' is not assignable to parameter of type 'string'.

Examples

export type TypeKeys<T, U> = {
    [P in keyof T]: U extends T[P] ? P : never;
}[keyof T];
export type FunctionKeys<T> = TypeKeys<T, (...args: any[]) => any>;

class Base {
    def = true;

    foo!: boolean;
    constructor<T extends Base>(this: T, data: Omit<T, 'def' | FunctionKeys<T>>) {
        Object.assign(this, data);
    }

    test() {}
}
class A extends Base {
    bar: number = 0;
}
class B extends Base {
    baz: string = 'hello';
}
new A({foo: true, bar: 123});
new B({foo: false, baz: 'world'});

Problems

Allowing constructors to have generic types does effectively create two sets of generics, which is an issue as mentioned here https://github.com/microsoft/TypeScript/issues/10860#issuecomment-246456721, but a number of solutions exist to this problem:

class Foo<T> {
  constructor<U>(x: T, y: U) { }
}

new Foo<number, string>(0, '') // A: concatenate generics
new Foo<number><string>(0, '') // B: separate type arguments
new (Foo<number>)<string>(0, "") // C: wrapped generic class

// https://github.com/microsoft/TypeScript/issues/10860#issuecomment-300918682
class Bar<T> {
  constructor<U = string>(x: T, y: U) { }
}
new Bar<number>(0, '') // D: only allow constructor generics if a default is provided

Alternatives

The initialization pattern is also currently achievable using a static method.

class Base {
    foo = false;

    // To prevent user from initializing directly request impossible parameters, since protected does not work
    constructor(internalGuard: never) {}

    static construct<T extends Base>(
            this: new (internalGuard: never) => T,
            params: T) :T {
        return Object.assign(new this(<never>undefined), params);
    }
}
class A extends Base {
    bar!: number;
}
A.construct({foo: true, bar: 1});

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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, die vorgeschlagene Konstruktorsyntax und die verknüpfte Diskussion in Issue 10860 zu prüfen, und untersuche anschließend, wie die Diagnosen TS1092 und TS2681 erzeugt werden. Als abgeschlossen gilt die Arbeit, wenn ein abgestimmtes Design erreicht und die Unterstützung der Typprüfung für generische Konstruktoren und this-Parameter von Konstruktoren implementiert wurde, einschließlich der Beispiele in diesem Issue.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
typescript
Bereich
compilers
Issue-Typ
Feature
Schwierigkeit
5/5
Geschätzter Aufwand
Über eine Woche
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
25/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.