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

Can not extend built in types #1168

Closed
mhegazy opened this issue Nov 14, 2014 · 14 comments
Closed

Can not extend built in types #1168

mhegazy opened this issue Nov 14, 2014 · 14 comments
Assignees
Labels
Bug A bug in TypeScript ES6 Relates to the ES6 Spec Fixed A PR has been merged for this issue

Comments

@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2014

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");
    }
}
@mhegazy mhegazy added Bug A bug in TypeScript ES6 Relates to the ES6 Spec labels Nov 14, 2014
@metaweta
Copy link

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.

@saschanaz
Copy link
Contributor

@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

@mhegazy
Copy link
Contributor Author

mhegazy commented Nov 18, 2014

@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.

@mhegazy
Copy link
Contributor Author

mhegazy commented Nov 18, 2014

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

@metaweta
Copy link

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

@unional
Copy link
Contributor

unional commented Jun 1, 2015

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

@mhegazy
Copy link
Contributor Author

mhegazy commented Jun 2, 2015

@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.

@unional
Copy link
Contributor

unional commented Jun 2, 2015

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

@ahejlsberg
Copy link
Member

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

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Jun 17, 2015
alexeagle added a commit to alexeagle/TypeScript that referenced this issue Jul 30, 2015
@kostrse
Copy link

kostrse commented Aug 23, 2015

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!

@DanielRosenwasser
Copy link
Member

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

@jpsfs
Copy link

jpsfs commented Sep 21, 2015

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!

@mhegazy
Copy link
Contributor Author

mhegazy commented Sep 21, 2015

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

@pleerock
Copy link

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript ES6 Relates to the ES6 Spec Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

10 participants