Typescript: Can not extend built in types

Created on 14 Nov 2014  ·  14Comments  ·  Source: microsoft/TypeScript

The ES6 designates some native objects as subclassable, e.g. Object, Boolean, Error, Map, Promise ..etc. The way these constructs are modeled in lib.d.ts makes is impossible to subclass, as they are defined as a pair of a var and an interface.

From the ES6 spec section19.5.1:

The Error constructor is designed to be subclassable. It may be used as the value of an extends clause of a class declaration. Subclass constructors that intended to inherit the specified Error behavior should include a super call to the Error constructor to initialize subclass instances.

Currently this would result in "A class can only extend another class" error:

class NotImplementedError extends Error {
    constructor() {
        super("Not Implemented");
    }
}
Bug ES6 Fixed

Most helpful comment

Even there is ability to extend Error class nowdays, Im still having inconvenience with it on node. For example Im having troubles with "error.stack". When I do throw new Error(...) I get error.stack, but when I do throw new CustomError() - I don't. To solve it I forced to use this trick:

export class HttpError extends Error {
    httpCode: number;
    message: string;

    constructor(httpCode: number, message?: string) {
        super();
        if (httpCode)
            this.httpCode = httpCode;
        if (message)
            this.message = message;

        this.stack = new Error().stack;
    }
}

All 14 comments

This is a general problem for any js library that exposes functions that are meant to be subclassable. If my library exports the Foo constructor and expects to receive instances of subclasses of Foo (not just objects implementing a Foo-like interface), there's currently no way to allow that.

I would like to be able to say

class Error;

There would be no javascript emitted for such a statement, only an update of Typescript's symbol table.

For the general case, I'd like to be able to replace the body of any class declaration with a semicolon, allowing things like

class Foo<X, Y> extends Bar<X> implements Baz<Y>;

which would mean that the library already guarantees that Foo.prototype is an instance of Bar.prototype and implements the Baz interface.

@metaweta I think it may make some redundancies:

interface Foo {
}
class Foo; // Foo is now a class!
interface Bar extends Foo {
}
class Bar extends Foo; // Two `extends Foo`s. 

interface X {
}
class X extends Foo; // Hmm?
interface Y extends Foo {
}
class Y; // Hmm?

It still have some potentials as the subclassable interfaces will work as open-ended classes. #819

@metaweta your first case should be handled by an ambient class declaration. e.g.

// myLib.d.ts
declare class Error {
}
// no code emitted here for this. 
// myfile.ts
// Error is subclassable,
class MyError extends Error {
}

For the second case, I would file a different suggestion issue for it.

Here is the full list of "subclassable" objects from the ES6 spec. currently these are all defined as an interface/var pairs.

  • Object
  • Boolean
  • Error
  • NativeError
  • Number
  • Date
  • String
  • RegExp
  • Array
  • TypedArray(Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, and DataView)
  • Map
  • Set
  • WeakMap
  • WeakSet
  • ArrayBuffer
  • DataView
  • Promise

Problems:

  • Once a type is defined as a class, there is no way to extend the member side (extending the static side can happen through a module).
  • Existing extensions to the member side will not work as you can not redefine a class (breaking change)
  • No way to model function calls on a class, e.g Object, String, Boolean, Date, Number, RegExp, Error, and Array. (again breaking change)

Possible solutions:

  • Allow extending any type with a prototype property and a construct signature
  • Allow extending instance side of a class (e.g. module Class.prototype {}, courtesy of @RyanCavanaugh )
  • Allow defining call signatures on a class constructor

@mhegazy Thanks, I was unaware of the ambient class declaration construct.

When I extend the Error class this way (using ambient class definition), throwing the ExtendedError does not produce stack trace nor assign message properly.

declare class Error {
  constructor(message?: string);
}

class ExtendedError extends Error {
  constructor(message?: string) {
    super(message + " extended");
  }
}

//throw new Error("Test");
throw new ExtendedError("Test");

@unional this looks like an engine issue. as per ES6 spec it should work. Engines should be fixing these now that ES6 has become ratified.

I managed to get it to work. In my js code that was working, I have the Error.captureStackTrace call but I take it out when I implement it in ts because Error declaration doesn't have it.

There is an example code:
It's an C#-like Exception that takes an innerException.

declare class Error {
    public name:string;
    public message:string;
    public stack:string;
    constructor(message?:string);
    static captureStackTrace(error: Error, constructorOpt: any);
}

class Exception extends Error {
    public innerException: Error;
    constructor(message?: string, innerException?: Error|string) {
        // Guard against throw Exception(...) usage.
        if (!(this instanceof Exception)) return new Exception(message, innerException);
        super();
        if (typeof Error.captureStackTrace === 'function') {
            //noinspection JSUnresolvedFunction
            Error.captureStackTrace(this, arguments.callee);
        }
        this.name = "Exception";
        if (innerException) {
            if (innerException instanceof Error) {
                this.innerException = innerException;
                this.message = message + ", innerException: " + this.innerException.message;
            }
            else if (typeof innerException === "string") {
                this.innerException = new Error(innerException);
                this.message = message + ", innerException: " + this.innerException.message;
            }
            else {
                this.innerException = innerException;
                this.message = message + ", innerException: " + this.innerException;
            }
        }
        else {
            this.message = message;
        }
    }
}

Fixed by #3516. Classes can now extend arbitrary expressions of constructor function types.

What release of TypeScript this fix is going to be shipped?
I quickly checked release labels for 1.5.3 and 1.5.4, and seems it hasn't been shipped yet, only in master for now.

EDIT:
Sorry, right now have noticed that the bug marked by milestone "TypeScript 1.6"

Thanks for your work!

@kostrse you can try our nighty releases of TypeScript 1.6 by running npm install -g typescript@next.

Hi,

I'm using Typescript 1.6.2 and my lib.es6.d.ts show Error (and Array, ...) as Interface not class.
Is this already fixed in 1.6.2?

Cheers!

@jpsfs the fix, as noted by @ahejlsberg in https://github.com/Microsoft/TypeScript/issues/1168#issuecomment-112955503 is to allow classes to extend arbitrary expressions of constructor function types.

Even there is ability to extend Error class nowdays, Im still having inconvenience with it on node. For example Im having troubles with "error.stack". When I do throw new Error(...) I get error.stack, but when I do throw new CustomError() - I don't. To solve it I forced to use this trick:

export class HttpError extends Error {
    httpCode: number;
    message: string;

    constructor(httpCode: number, message?: string) {
        super();
        if (httpCode)
            this.httpCode = httpCode;
        if (message)
            this.message = message;

        this.stack = new Error().stack;
    }
}
Was this page helpful?
0 / 5 - 0 ratings