Skip to content

Allow deriving from object and intersection types#13604

Merged
ahejlsberg merged 9 commits into
masterfrom
intersectionBaseTypes
Jan 21, 2017
Merged

Allow deriving from object and intersection types#13604
ahejlsberg merged 9 commits into
masterfrom
intersectionBaseTypes

Conversation

@ahejlsberg
Copy link
Copy Markdown
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.

Comment thread src/compiler/checker.ts

// A valid base type is any non-generic object type or intersection of non-generic
// object types.
function isValidBaseType(type: Type): boolean {
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Member Author

@jwbay Yes, thanks. Now fixed.

Copy link
Copy Markdown
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
Copy Markdown

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: {