Extends Keyword on Object Type Literals

Topics: Language Specification
Oct 7, 2013 at 3:35 PM
Apologies if I'm bringing up an old topic here:

The are times I prefer to inline my object types rather than give them a name. This is especially true in declaration files because I'd rather not give names to types that don't really exist. For example:
declare module esri.dijit {
    export class Basemap {
        id: string;
        thumbnailUrl: string;
        title: string;
        constructor(params?: {
            id?: string;
            layers: esri.dijit.BasemapLayer[];
            thumbnailUrl?: string;
            title?: string;
        });
        getLayers(): esri.layers.Layer[];
    }
}
Similarly sometimes I have a function that takes in a known type, but for whatever reason also expects an extra member in there. Here's a slightly contrived example:
function processError(error) {
    console.log(error.code + ": " + error.message);
}
What I would like to be able to do is this:
function processError(error: {code: number;} extends Error): void {
    console.log(error.code + ": " + error.message);
}
note the extends keyword after the object type. Instead what I have to do is this:
interface ErrorWithCode extends Error {
    code: number;
}
function processError(error: ErrorWithCode): void {
    console.log(error.code + ": " + error.message);
}
which I consider less elegant if the type is only used in one place.

Could we see this usage of extends become legal for improved code elegance? Is there some ambiguity or confusion I'm not thinking of?
Oct 10, 2013 at 3:24 PM
My friend pointed out that I was vague in what I meant about "types that don't really exist."

Inside my example:
declare module esri.dijit {
    export class Basemap {
        id: string;
        thumbnailUrl: string;
        title: string;
        constructor(params?: {
            id?: string;
            layers: esri.dijit.BasemapLayer[];
            thumbnailUrl?: string;
            title?: string;
        });
        getLayers(): esri.layers.Layer[];
    }
}
That params field in the constructor is an object for passing in optional parameters and that's what I'm referring to. Another good name for it would be anonymous type.
Coordinator
Oct 11, 2013 at 8:55 PM
To summarize, are you asking for a way of creating a type literal that extends a named type?

While I can see some utility to this, I think the resulting code would actually look more complicated than just giving those types a name, but perhaps you have some common patterns in mind where this actually ends up being cleaner than using a name?
Oct 11, 2013 at 11:02 PM
Yeah I think that's a good summary of what I'm asking for.

Here's one real-world example from a declaration file I have where I think it might make sense:
    export class Locator {
        // ... snip
        on(event: string, handler: (event: { target: Locator; }) => void ): void;
        on(event: "address-to-locations-complete", handler: (event: { target: Locator; id: string; addresses: esri.tasks.AddressCandidate[]; }) => void ): void;
        on(event: "addresses-to-locations-complete", handler: (event: { target: Locator; id: string; addresses: esri.tasks.AddressCandidate[]; }) => void ): void;
        on(event: "error", handler: (event: { target: Locator; id: string; error: Error; }) => void ): void;
        on(event: "location-to-address-complete", handler: (event: { target: Locator; id: string; address: esri.tasks.AddressCandidate; }) => void ): void;
    }
This is a pattern for event handling where a single function attaches the listener, and the shape of the handler is determined by the name of the event. (Overload on constants is awesome btw.) The handler takes a single object which has a default property and may have more depending on the event. Right now the code doesn't really capture the relationship because the target: Locator element is repeated.

I think the following would look nicer:
    export interface LocatorEvent {
        target: Locator;
        id: string;
    }
    export interface ErrorEvent {  // common to many classes
        error: Error;
    }
    export class Locator {
        // ... snip
        on(event: string, handler: (event: LocatorEvent) => void ): void;
        on(event: "address-to-locations-complete", handler: (event: { addresses: esri.tasks.AddressCandidate[]; } extends LocatorEvent) => void ): void;
        on(event: "addresses-to-locations-complete", handler: (event: { addresses: esri.tasks.AddressCandidate[]; } extends LocatorEvent) => void ): void;
        on(event: "error", handler: (event: {} extends LocatorEvent, ErrorEvent) => void ): void;
        on(event: "location-to-address-complete", handler: (event: { address: esri.tasks.AddressCandidate; } extends LocatorEvent) => void ): void;
    }
This is less concise than the code that doesn't capture the relationship of the LocatorEvent but it's more concise than the code that gives every handler parameter a named type.

The other thing I don't like about naming a type here is that in a concrete class the interface declarations are either at the top or the bottom. If the class is large that puts them far away from their usage which is less than ideal.

If that's not convincing I'll come up with an example later when I find it where such a type is used within a function and never exposed to the outside world.
Oct 13, 2013 at 12:33 PM
@Grajkowski,

To be honest I don't see much gain in writing
interface LocatorEvent {
        target: Locator;
        id: string;
}
interface ErrorEvent {
        error: Error;
}
class Locator {
   on(event: "error", handler: (event: {} extends LocatorEvent, ErrorEvent) => void ): void;
}
instead of
interface LocatorEvent {
    target: Locator;
    id: string;
}
interface ErrorEvent extends LocatorEvent  {
   error: Error;
}
class Locator {
     on(event: "error", handler: (event: ErrorEvent) => void ): void;
}
Even if they are being used only within a single file, explicitly modelling the parameters is probably the right thing to do.
Oct 15, 2013 at 3:19 PM
@nabog I think the parameters are explicitly modeled either way. The difference is type doesn't have a name (pros and cons) and less typing (always a win).

How much typing does it save? Well a bunch of this:
    export interface ErrorEvent {   // common to many classes
        error: Error;
    }
    
    export interface LocatorEvent {
        target: Locator;
        id: string;
    }
    export interface LocatorAddressToLocationsCompleteEvent extends LocatorEvent {
        addresses: esri.tasks.AddressCandidate[];
    }
    export interface LocatorAdressesToLocationsCompleteEvent extends LocatorEvent {
        addresses: esri.tasks.AddressCandidate[];
    }
    export interface LocatorErrorEvent extends ErrorEvent, LocatorEvent {
    }
    export interface LocatorLocationToAddressCompleteEvent {
        address: esri.tasks.AddressCandidate;
    }
    export class Locator {
        // ... snip
        on(event: string, handler: (event: LocatorEvent) => void ): void;
        on(event: "address-to-locations-complete", handler: LocatorAddressToLocationsCompleteEvent): void;
        on(event: "addresses-to-locations-complete", handler: (event: AdressesToLocationsCompleteEvent): void;
        on(event: "error", handler: (event: LocatorErrorEvent) => void ): void;
        on(event: "location-to-address-complete", handler: (event: LocatorLocationToAddressCompleteEvent) => void ): void;
    }
And this is the example with a small number of events. Yeah it's not the end of the world but it strikes me as less elegant.

Either way this isn't a make-or-break feature. I just think it would be nice to have.
Oct 15, 2013 at 4:25 PM
@Grajkowski, I follow you.

What does the calling code look like? I'd assume:
var locator = new Locator();
locator.on("error", event => {
    // Handle error
});
But what if the caller doesn't want to use a lambda?
function errorHandler(event: LocatorErrorEvent){
    // handle error
}
var locator = new Locator();
locator.on("error", errorHandler);
If the type LocatorErrorEvent is not defined (i.e. only defined inline as per your suggestion) then the caller has to type the errorHandler parameter manually themselves or leave it at any.

(BTW I think you have defined LocatorAddressToLocationsCompleteEvent twice in your code)
Oct 15, 2013 at 5:17 PM
But what if the caller doesn't want to use a lambda?
Then they have to type out or copy and paste the types. This for example does kind of suck:
function errorHandler(event: {} extends LocatorEvent, ErrorEvent): void {
    // handle error
}
function completeHandler(event: { addresses: esri.tasks.AddressCandidate[]; } extends LocatorEvent): void {
    // handle complete
}
var locator = new Locator();
locator.on("error", errorHandler);
locator.on("address-to-locations-complete", completeHandler);
However the fewer "invented" types that don't occur in the original JavaScript the easier it is to switch between declaration files. If someone else were to write declaration files for the same library they would likely use different names and might not even name all the same concepts.
I think you have defined LocatorAddressToLocationsCompleteEvent twice in your code
LocatorAddressToLocationsCompleteEvent and LocatorAdressesToLocationsCompleteEvent just look very very similar.