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

Allow deriving from object and intersection types #13604

Merged
merged 9 commits into from
Jan 21, 2017

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Jan 20, 2017

With this PR we permit classes and interfaces to derive from object types and intersections of object types. Furthermore, in intersections of object types we now instantiate the this type using the intersection itself. Collectively these changes enable several interesting "mixin" patterns.

In the following, a type is said to be object-like if it is a named type that denotes an object type or an intersection of object types. Object-like types include named object literal types, function types, constructor types, array types, tuple types, mapped types, and intersections of any of those.

Interfaces and classes may now extend and implement types as follows:

  • An interface is permitted to extend any object-like type.
  • A class is permitted to extend an expression of a constructor type with one or more construct signatures that return an object-like type.
  • A class can implements any object-like type.

Some examples:

type T1 = { a: number };
type T2 = T1 & { b: string };
type T3 = () => void;
type T4 = [string, number];

interface I1 extends T1 { x: string }  // Extend object literal
interface I2 extends T2 { x: string }  // Extend intersection
interface I3 extends T3 { x: string }  // Extend function type
interface I4 extends T4 { x: string }  // Extend tuple type

An interface or class cannot extend a naked type parameter because it is not possible to consistently verify there are no member name conflicts in instantiations of the type. However, an interface or class can now extend an instantiation of a generic type alias, and such a type alias can intersect naked type parameters. For example:

type Named<T> = T & { name: string };

interface N1 extends Named<T1> { x: string } // { a: number, name: string, x: string }
interface N2 extends Named<T2> { x: string } // { a: number, b: string, name: string, x: string }

interface P1 extends Partial<T1> { x: string } // { a?: number | undefined, x: string }

The this type of an intersection is now the intersection itself:

interface Thing1 {
    a: number;
    self(): this;
}

interface Thing2 {
    b: number;
    me(): this;
}

function f1(t: Thing1 & Thing2) {
    t = t.self();  // Thing1 & Thing2
    t = t.me().self().me();  // Thing1 & Thing2
}

All of the above can be combined in lightweight mixin patterns like the following:

interface Component {
    extend<T>(props: T): this & T;
}

interface Label extends Component {
    title: string;
}

function test(label: Label) {
    const extended = label.extend({ id: 67 }).extend({ tag: "hello" });
    extended.id;  // Ok
    extended.tag;  // Ok
}

Also, mixin classes can be modeled, provided the base classes have constructors with a uniform shape:

type Constructor<T> = new () => T;

function Identifiable<T>(superClass: Constructor<T>) {
    class Class extends (superClass as Constructor<{}>) {
        id: string;
        getId() {
            return this.id;
        }
    }
    return Class as Constructor<T & Class>;
}

class Component {
    name: string;
}

const IdentifiableComponent = Identifiable(Component);

class Box extends IdentifiableComponent {
    width: number;
    height: number;
}

const box = new Box();
box.name;
box.id;
box.width;
box.height;

We're still contemplating type system extensions that would allow the last example to be written without type assertions and in a manner that would work for arbitrary constructor types. For example, see #4890.

EDIT: Mixin classes are now implemented by #13743.

Fixes #10591.
Fixes #12986.

@@ -3951,15 +3956,22 @@ namespace ts {
return true;
}

// A valid base type is any non-generic object type or intersection of non-generic
// object types.
function isValidBaseType(type: Type): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type could be : type is BaseType ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, BaseType is the most restrictive type we can express for a valid base type, but there are still BaseType instances that aren't valid base types (e.g. intersections containing type parameters). So, wouldn't be correct in the negative sense.

@jwbay
Copy link
Contributor

jwbay commented Jan 20, 2017

In the second block of examples, are N1 and N2 missing x: string in the commented result?

@ahejlsberg
Copy link
Member Author

@jwbay Yes, thanks. Now fixed.

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some tests for the changes in the apparent type for this in intersection types.

@ahejlsberg ahejlsberg merged commit 5b9004e into master Jan 21, 2017
@ahejlsberg ahejlsberg deleted the intersectionBaseTypes branch January 21, 2017 21:38
@rotemdan
Copy link

rotemdan commented Jan 22, 2017

@ahejlsberg

I'm not 100% sure but it seems like this change breaks a common pattern I use (and probably others, at least in the future) with React. This worked in yesterday's nightly build:

export class MyComponent extends React.Component<{ someValue: number }, {}> {
	shouldComponentUpdate(nextProps: this["props"], nextState: this["state"]) {
		if (nextProps.someValue > 0) {
			// ...
		}
	}
}

But now errors:

// Error: Property 'someValue' does not exist on type 'this["props"]'.'

The problem might be related to the fact that this["props"] has the type:

{
    children?: React.ReactNode;
} & {
    someValue: number;
}

I've tried to work around this by using typeof this.props instead but that didn't seem to help:

export class MyComponent extends React.Component<{ someValue: number }, {}> {
	shouldComponentUpdate(nextProps: typeof this.props, nextState: typeof this.state) {
		if (nextProps.someValue > 0) {
			// ...
		}
	}
}

// Error on 'typeof this.props': Identifier expected

The relevant ambient declaration for React.Component looks like:

    // Base component for plain JS classes
    class Component<P, S> implements ComponentLifecycle<P, S> {
        constructor(props?: P, context?: any);
        constructor(...args: any[]);
        // ...
        props: { children?: ReactNode } & P;
        state: S;
        // ...
    }

@ahejlsberg
Copy link
Member Author

@rotemdan Yes, there was a minor typo that's causing this issue. I will have a fix shortly.

@rotemdan
Copy link

rotemdan commented Jan 22, 2017

Thanks, I temporarily worked around these errors by creating a type alias for this[props], which was relatively easy. I'm intentionally using the latest nightly builds with a large code base to try to help to quickly identify issues of this kind. If I update every day it makes it easy to speculate what could have been the cause by looking at the commits of the previous day.

@shlomiassaf
Copy link

shlomiassaf commented Jan 23, 2017

@ahejlsberg this is just CRAZY!

I can now build a decorator, export it as const and export a type with the same name.
The type is a Record of a name and the decorators expected signature... I now have a dynamic typed decorator, perfect for lifescycle hooks as decorators.

export class Class implements MyDecorator<'myMethod'> {
  @MyDecorator()
  myMethod() { // myMethod's signature is fixed, if changed -> type error! if name changes -> type error
  }
}

This is getting to be the best type system ever, I am really amazed! thank you guys!

@stanhuff
Copy link

It would be very nice if a class decorator could "replace" the original class type with respect to the type system so that we could use such a decorator to fully implement a mixin, both the implementation at runtime and the extended type for compile time.

@atrauzzi
Copy link

Forgive me if it's a silly question, but would this change help at all with what I'm looking for here?

@trusktr
Copy link
Contributor

trusktr commented Jan 26, 2017

Brand new to TS here. Is there a simple way to make this work?

function Foo() {}

class Bar extends Foo {
    constructor() {
        super()
        console.log('Bar!')
    }
}

new Bar

@mhegazy
Copy link
Contributor

mhegazy commented Jan 26, 2017

A class needs to inherit from something that has a "construct signature"; i.e. a declaration of what the shape of the object it returns if called with new. a function does not have that.

if this is your function, i would suggest switching it to a class, and switching this.prop assignments in it to property declarations.

if this is a function you got from a different module, then consider declaring it as a calss, e.g. declare class Foo {}.

you can always cast the type to a constructable type, e.g. const Foo: { new (): FooInstance } = <any>function Foo() { }

@trusktr
Copy link
Contributor

trusktr commented Jan 27, 2017

if this is your function, i would suggest switching it to a class
if this is a function you got from a different module, then consider declaring it as a calss

In reality it is a dynamically generated class as in the following (working) cases:

class Foo extends multiple(One, Two, Three) {}
class Foo extends MixinOne(MixinTwo(Three)) {}

I am relying on those features to do interesting things. These type of things are possible in JS.

Can I use interfaces somehow?

@trusktr
Copy link
Contributor

trusktr commented Jan 27, 2017

Oh, another thing. I know how to declare a type, for example, so that I can extend from a Backbone.View, by declaring the class definition.

But how do I declare that some function returns a specific class?

F.e.,

// Third party
function get(){
  return Backbone.View
}

// My file
Class Foo extends get() {}

@mhegazy
Copy link
Contributor

mhegazy commented Jan 27, 2017

@trusktr you need #4890, this should land in the next few days.

@trusktr
Copy link
Contributor

trusktr commented Jan 30, 2017

@mhegazy Once released, how would I write that last example? Is the following correct?

interface Constructable<T> {
    new (...args): T;
}

// Third party
function get(): Constructable<Backbone.View> {
  return Backbone.View
}

// My file
Class Foo extends get() {}

What about the following?

function multiple(...classes: whatGoesHere?): Constructable<whatGoesHere?> {
  // uses Proxy
}

class Foo extends multiple(One, Two, Three) {}

@mhegazy
Copy link
Contributor

mhegazy commented Jan 30, 2017

Please see #13743

@felixfbecker
Copy link
Contributor

Does this also allow extending from typeof expressions?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet