Skip to content

Can not extend built in types #1168

Description

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");
    }
}

Activity

  1. metaweta commented on Nov 17, 2014

    @metaweta

    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.

  2. saschanaz commented on Nov 18, 2014

    @saschanaz
    Contributor

    Mike Stay (@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

  3. mhegazy commented on Nov 18, 2014

    @mhegazy
    ContributorAuthor

    Mike Stay (@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.

  4. mhegazy commented on Nov 18, 2014

    @mhegazy
    ContributorAuthor

    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 Ryan Cavanaugh (@RyanCavanaugh) )
    • Allow defining call signatures on a class constructor
  5. metaweta commented on Nov 18, 2014

    @metaweta

    Mohamed Hegazy (@mhegazy) Thanks, I was unaware of the ambient class declaration construct.

  6. unional commented on Jun 1, 2015

    @unional
    Contributor

    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");
    
  7. mhegazy commented on Jun 2, 2015

    @mhegazy
    ContributorAuthor

    Homa Wong (@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.

  8. unional commented on Jun 2, 2015

    @unional
    Contributor

    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;
            }
        }
    }
    
  9. ahejlsberg commented on Jun 17, 2015

    @ahejlsberg
    Member

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

  10. added a commit that references this issue on Jul 30, 2015
  11. kostrse commented on Aug 23, 2015

    @kostrse

    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!

  12. DanielRosenwasser commented on Aug 24, 2015

    @DanielRosenwasser
    Member

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

  13. jpsfs commented on Sep 21, 2015

    @jpsfs

    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!

  14. mhegazy commented on Sep 21, 2015

    @mhegazy
    ContributorAuthor

    José Pedro Silva (@jpsfs) the fix, as noted by Anders Hejlsberg (@ahejlsberg) in #1168 (comment) is to allow classes to extend arbitrary expressions of constructor function types.

  15. pleerock commented on May 15, 2016

    @pleerock

    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;
        }
    }
  16. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptES6Relates to the ES6 SpecFixedA PR has been merged for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions