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

Proposal: Merged Declarations for Classes and Interfaces #3332

Closed
aozgaa opened this issue Jun 1, 2015 · 0 comments
Closed

Proposal: Merged Declarations for Classes and Interfaces #3332

aozgaa opened this issue Jun 1, 2015 · 0 comments
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@aozgaa
Copy link
Contributor

aozgaa commented Jun 1, 2015

Proposal for Merged Declarations between Classes & Interfaces

This is a proposal for a new feature, merging class/interface declarations, to allow existing libraries to remain type-compatible with dependencies that migrate from interface to class declarations as part of the process of updating to ES6.

Introduction

Consider lib1.ts and lib2.ts, where lib2.ts depends on and extends an interface in lib1.ts. Eg:

// lib1.ts
interface Foo {
    bar : number;
}

// lib2.ts
interface Foo {
    baz(x : number) : boolean;
}

Today, through merged interface-interface declarations, lib2.ts can write the above code to extend the interface Foo. The prototypical example of this is in polyfill libraries, in particular those modifying standard built-in objects, such as Array<T>. As part of the tsc compiler, we provide interface declarations for Array<T> in src/lib/core.d.ts (ES3, ES5) and src/lib/es6.d.ts (ES6).

To extend Array<T>, say with a proposed ES7 member function like includes() (description available here), a library/polyfill creator might write

// fixes the type of Array<T>
interface Array<T> {
    includes(searchElement : T, fromIndex : number) : boolean;
}

// provides the implementation
Array.prototype.includes = function(searchElement : T, fromIndex : number) : boolean {
    // implementation ...
}

and then use the polyfilled Array<T> normally.

With the advent of ES6, we would like to modify the API so that Array<T> is now declared a class instead of an interface. In ES6, built-in's can be subclassed, so the current API (with interface declarations) is difficult if not impossible to maintain while remaining consistent with ES6. Additionally, class-based declarations are cleaner. Since classes do not merge with interface declarations presently, this would present a breaking change to the API -- the above code would trigger an error. To allow for a more straightforward transition to ES6 declarations while retaining backwards compatibility, we propose merged declarations of ambient classes and interfaces.

Details of Proposal

If an object is declared twice, once as an ambient class and then as an interface, then the resulting object is a class whose fields are both those of the class declaration and the interface declaration. The interface fields are marked public in the resulting type. On other words, the type of the class object is merged with the type of the interface. The class constructor object is unmodified.

Conflicts among the members/properties of the resulting type are resolved as if the object were declared contiguously within the class declaration.

The order of the declarations is irrelevant (ie: the behavior is the same regardless of whether the interface or class is declared first). Moreover A class can be merged with an arbitrary number of interfaces with the same name. For example,

declare class Foo {
    private x : number;

}

interface Foo {
    public y : string;
}

function bar(foo : Foo)  {
    foo.x = 1;
    foo.y = "1"; // okay, declared above.
    foo.z = true; // okay, declared below.
}

interface Foo {
    z : boolean;
}

resolves the type of Foo as

{ x : number, y : string, z : boolean }

This proposal does not extend to allow merged declarations of classes-classes, classes-namespaces, or classes-functions. Moreover, since this applies only for the ambient portion class declarations, this proposal has no runtime behavior on the resulting type.

Pros

  • Library writers can update to ES6 idioms without breaking backwards compatibility.
  • Users of ES6 syntax (ie: class declarations) can interoperate with interface-syntax in TypeScript when declaring types. A user of a vanilla ES6 library with a class declaration on some object Foo may want to polyfill a class. interface-class merged declarations allow for this facility (where the type is obtained from the library's corresponding .d.ts file).
  • Generally, this allows the programmer to re-open and modify a class' declared type.

Cons

  • Before, an unintentional name-conflict between an interface and a class would be a caught as an error. Now, this triggers perhaps surprising behavior in the type system. Perhaps it would be useful to limit the contexts in which class-interface merging can be performed, either with an additional keyword mergeable or a compiler flag?
  • If a type Foo is declared an interface, it is no longer a priori clear if Foo is actually an interface or a class or both. This may make some language service completion counter-intuitive.

To Be Discussed

  • For the key example, it isn't necessary that class-interface merging be symmetric. For our examples, the first declaration is always the class. Should the order of the merged declarations matter?
  • Should this feature be extended to non-ambient class declarations? It is unclear how the language would need to be modified to allow the programmer to open up the constructor to initialize new values.
  • How should type deductions perform across files/the points between the declarations? For example,
// file1.ts
declare class C {
    public x : number;
}

function funcI(c : C) {
    console.log(c.x);
    console.log(c.y); // okay?
    console.log(c.z); // okay?
}

interface C {
    y : number;
}

// file2.ts
interface C {
    z : number;
}

For the analogous code sample with merged declarations for interfaces, the type is resolved across both files, so both lines are okay. Do we want to emulate this behavior (ie: that the type is inferred globally for each usage)?

@aozgaa aozgaa added Suggestion An idea for TypeScript In Discussion Not yet reached consensus Committed The team has roadmapped this issue labels Jun 1, 2015
@aozgaa aozgaa self-assigned this Jun 1, 2015
@aozgaa aozgaa added this to the TypeScript 1.6 milestone Jun 1, 2015
@RyanCavanaugh RyanCavanaugh removed the In Discussion Not yet reached consensus label Jun 8, 2015
@aozgaa aozgaa closed this as completed Jul 2, 2015
@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Aug 7, 2015
kwonoj added a commit to kwonoj/rxjs that referenced this issue Dec 7, 2015
- remove circular references using
  microsoft/TypeScript#3332, parents are
references children via ambient interfaces and let compiler merges it
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants