Typescript: Suggestion: Reopen static and instance side of classes

Created on 29 Apr 2015  路  10Comments  路  Source: microsoft/TypeScript

Summary

To support both the semantics of subclassing built-ins in ES6 and still allow authors to augment built-ins, we need a mechanism to reopen the static and instance sides of a class.

Current state

Today we can re-open interfaces, allowing authors to augment built-ins (for example, to support polyfills):

// in lib.d.ts
interface Array<T> { /*...*/ }
interface ArrayConstructor { /*...*/ }
declare var Array: ArrayConstructor;

// in polyfill.ts
interface Array<T> {
  includes(value: T): Boolean;
}
interface ArrayConstructor {
  of<T>(...items: T[]): Array<T>;
}

Array.prototype.includes = function (value: any) { return this.indexOf(value) != -1; }
Array.of = function<T> (...items: T[]) { return items; }

We can also re-open the static side of a class, in a limited fashion:

// initial declaration
class MyClass {
}

// re-open
module MyClass {
  export var staticProperty = 1;
}

There are several issues with these approaches:

  • You cannot use type defined by the var/interface pattern in the extends clause of a class in TypeScript, meaning that "classes" defined using this pattern cannot be subclassed in ES6, which is an issue for built-ins.
  • While you can re-open the static side of a class using module, you can only use non-keyword identifiers for property names. So you could not, for example, add a [Symbol.species] property to the class, or use decorators on these members.
  • There is no way to re-open the instance side of a class.

Proposal

I propose we add a new syntactic modifier for the class declaration that would indicate we are re-opening an existing class. For this example I am using the keyword partial, although the semantics here differ significantly than the same-named capability in C#:

// in lib.d.ts
declare class Array<T> {
}

// in polyfill.ts
partial class Array<T> {
  static of<T>(...items: T[]) { return items; }
  includes(value: T): boolean { return this.indexOf(value) != -1; }
}

// emit (ES5)
Array.of = function() { 
  var items = [];
  for (var _i = 0; i < arguments.length; i++) 
    items[i] = arguments[i];
  return items;
}
Array.prototype.includes = function(value) {
  return this.indexOf(value) != -1;
}

Rules

  • A partial class declaration must be preceded by a non-partial class declaration in the same lexical scope. These should be the same rules that apply when merging a module with a class or function today.
  • A partial class declaration must have the same module visibility as the preceding non-partial class declaration.
  • A partial class declaration must have the same generic type parameters (including constraints) as the non-partial class declaration.
  • A partial class declaration cannot have an extends clause, but may have an implements clause.
  • A partial class declaration cannot have a constructor member.
  • A partial class declaration cannot have members with the same name as existing members on a class.

    • Exception: ambient partial class declaration members can merge with other ambient partial class declaration members if they are compatible overloads, similar to interfaces.

  • Non-static property declarations on a partial class declaration cannot have initializers.
  • A partial class declaration can have a class decorator. User code that executes in-between the initial class declaration and the partial declaration will be able to observe the class before decorators on the partial class are applied.

    • NOTE: We could choose to disallow class decorators on a partial class.

      Out of scope

  • This proposal does not cover the case where built-in "classes" can often also be called as functions. This case is covered in #2959.

  • This proposal does not cover the case where authors may have already extended the interface of a built in. This case is covered in #2961.

    Previous discussions

This has also been discussed previously:

In Discussion Suggestion

Most helpful comment

I would like this, because for example, if a 3rd-party type definition is not accurate, I would like to augment the class in order to quickly fix it for my case.

In my case, the constructor of a class is stated to receive an argument of one type, but in reality it can accept an argument of a union of two types, and I'd like to simply fix this without having to fork a library or rewrite the entire definition of the class.

All 10 comments

Note comments in #563

Also worth linking #9 since it covers basically the same use cases

yes, please!

Need to write up the status of this one

I like this suggestion, but would like to see it extended slightly.

Say I have a class generated from a C# class. I would like to be able to apply decorators to properties of the class without having to change the generated code.

I could see this working, if I am allowed to add an existing property to the partial class with the same type or a more general compatible type (so any would work in all cases). The property would _not_ change type.

Example:

// in Person.ts (a generated file from somewhere)
export class Person {
    @key
    id: number;
    name: string;
}

// in PersonExtensions.ts
import { Person } from './Person';

partial class Person {
    @required
    name: any;
}

+1

In JS you do this by extending the prototype. While a friendlier syntax would be welcome, supporting the traditional style would be great for those who need it. TS is rumored to be a superset of JS after all 馃檪
~js
// [ts] Property 'foo' does not exist on type 'Klass'.
Klass.prototype.foo = function() { console.log("foo!"); }
new Klass().foo();
~

I would like this, because for example, if a 3rd-party type definition is not accurate, I would like to augment the class in order to quickly fix it for my case.

In my case, the constructor of a class is stated to receive an argument of one type, but in reality it can accept an argument of a union of two types, and I'd like to simply fix this without having to fork a library or rewrite the entire definition of the class.

Is there any progression on this topic? I have the same problem as @trusktr .

Random bikeshed as the original partial class proposal violates the design goal:

declare class Foo {};
interface class Foo {
  staticMethod(): boolean;
};

let result = Foo.staticMethod(); // boolean
Was this page helpful?
0 / 5 - 0 ratings

Related issues

kyasbal-1994 picture kyasbal-1994  路  3Comments

dlaberge picture dlaberge  路  3Comments

uber5001 picture uber5001  路  3Comments

MartynasZilinskas picture MartynasZilinskas  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments