Typescript: Suggestion: Interface extend generic type

Created on 6 Mar 2015  路  37Comments  路  Source: microsoft/TypeScript

Hi all,

I was hoping that typescript could support interface extending generic types like the following example:

    interface IHttpPromise<T extends {}> extends T{
        $resolved: boolean;
        $error: boolean;
    }

    interface IHttpPost {
        <T extends {}>(actionUrl: string, postData: any): IHttpPromise<T>;
    }

The use case is that an http request is initiated and the IHttpPost method returns an object which gets populated when the request completes. In angular this pattern is being used by the $resource service (see definitelyTyped IResource declaration here).

Is this feasible?

Thanks in advance,

Andreas

PS: I have seen this issue in codeplex but couldn't find it here.

Needs Proposal Suggestion

Most helpful comment

I think I found a solution for this:

  export type Instance<TInstance extends {}> = TInstance & {
    save(fn: (err: Error) => void): void;
  };

This can be used like:

find(fn: (err: Error, result: Instance<IAnimal>) => void): void;

find(function(err, animal) {
  animal.save((err) => void 0) // no problem
});

Still not optimal, but better.

All 37 comments

I think it is actually quite common in JS to take an object, add properties or functions to it and return it. And I wouldn't know how else to define the return as a generic except

getRemoteObject<T>(id: string): IRemoteObject<T>;

interface IRemoteObject<T> extends T { save(); getRawObject(): T; ... }

I've just written a definition file for such a similar case and had to discribe in the comments the workaround to actually create an interface yourself which merges IRemoteObject<T> and T. For instance:

interface IUser {email: string; name: string; ...}
interface IRemoteUser extends IRemoteObject<IUser>, IUser { }

var user: IRemoteUser = getRemoteObject<IUser>('id');

Or did I miss any other way to implement this?

+1! I think this is the best way to use TypeScript with Immutable js records. Also generally when you want to use the decorator pattern on generic types, such as with Radium.enhancer().

Here's another example:

interface Animal {
  numberOfLegs: number;
}

interface MaleAnimal<A extends Animal> extends A {}

interface FemaleAnimal<A extends Animal, M extends MaleAnimal<A>> extends A {
  mate(father:M): Promise<A[]>;
}

This has two errors:

  • An interface may only extend a class or another interface.
  • Constraint of a type parameter cannot reference any type parameter from the same type parameter list.

Why not use intersection types to model this? e.g.

interface Controller {
    name: string;
}

interface EventHandler {
    handel(event: string): void;
}

var MyEventHandelingControler: Controller & EventHandler;

MyEventHandelingControler.name;
MyEventHandelingControler.handel("event");

Is this a suggestion his it could be implemented, or already a working feature?

@pgrm, intersection types is part of TypeScript 1.6, here is the release blog

Will I be able to do something like this:

function addMetadata<T>(data: T): T & MetaData {
     ....
}

@ronzeidman yes, that should work.

Yes the example seems to suggest that it should work also with generics:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result;
}

var x = extend({ a: "hello" }, { b: 42 });
var s = x.a;
var n = x.b;

That solves what I wanted. So from my side this issue could be closed. thx @mhegazy

@andreasbotsikas, any other scenarios not captured in the previous discussion?

Yes and I like the fact that I transfer the logic to the function instead of declaring an interface that combines the 2 interfaces. My sample works with the following:

interface IHttpPromiseInfo{
     $resolved: boolean;
     $error: boolean;
}

interface IHttpPost {
        <T extends {}>(actionUrl: string, postData: any): IHttpPromiseInfo & T;
}

Thanks for bringing this feature!

While certain use-cases have been addressed, I'm not sure that this should be closed.

@DanielRosenwasser I agree. For example:

interface Animal {
  numberOfLegs: number;
}

interface Male {
  yChromosome: any;
}

interface Female<A> extends A { //Error: An interface may only extend a class or another interface.
  mate(male:Male & A): (A & (Male | Female<A>))[];
}

Still gets an error of: An interface may only extend a class or another interface.

This would be a useful feature for working with OData GET responses.

http://www.odata.org/ > Step 2. Requesting an individual resource

OData mixes its own fields with a single response's fields. For example, given the following interface...

interface IPerson {
    FirstName: string;
    LastName: string;
}

...an OData response for a single entity would be...

// Shortened for brevity
{
    "@odata.context":"http://my-api#People/$entity(0000-0000-0000-0000)",
    "FirstName":"Russell",
    "LastName":"Whyte"
}

Right now, the best solutions for typing that response in a .d.ts would be having an IODataReponse contain "@odata.context"-style fields with an IPersonResponse extends IPerson, IODataResponse, or an intersection type:

interface IODataResponse {
    "@odata.context": string
}

// Yuck!
interface IPersonResponse extends IPerson, IODataResponse { }
// Less yuck
var response: IPerson & IODataResponse;

A much easier (and more elegant IMO) solution would be to have IODataResponse template a generic T and extend from that T:

interface IODataResponse<T> extends T { }

var response: IODataResponse<IPerson>;

@JoshuaKGoldberg Good real world example.

I think I found a solution for this:

  export type Instance<TInstance extends {}> = TInstance & {
    save(fn: (err: Error) => void): void;
  };

This can be used like:

find(fn: (err: Error, result: Instance<IAnimal>) => void): void;

find(function(err, animal) {
  animal.save((err) => void 0) // no problem
});

Still not optimal, but better.

+1

+1

+1

@louy GREAT!!!

Another case where this would help ergonomics is when coupling it with classes. For example, imagine an ORM:

class Model<Attributes> {
  constructor(attributes:Attributes);
  // ...
}
interface Model<Attributes> extends Attributes {}

// ---

interface UserAttributes {
  id:number;
  name:string;
}

class User extends Model<UserAttributes> {
  firstName() {
    return this.name.split(/\s+/)[0]; // `name` should be available.
  }
}

Just came across a need for this when declaring a base entity class as part of a setup to work with Azure Table Storage. I want to create a base entity class that absorbs all the properties of the data passed into it. That base class has a generic parameter of the data it receives.

So ultimately that generic parameter is what needs to be implemented/extended.

@louy Except that it cannot be implemented by a class:

class Foo implements Instance<Bar> {...}

[ts] A class may only implement another class or interface.

@andraaspar yes that's why it's not optimal. it's a type not an interface.

i think this can be done in a clean way with keyof now though.

I have a case where a method converts an removes some properties of a base class, which should also work on interfaces inherited from that class. The inherited interfaces would then change the base class they're inherited from:

interface A {a: string}
interface AA extends A {b: string}
interface AB extends A {b_conv: string}

// Converts any type that inherits AA to the same type inheriting AB
const converter = <T extends AA>(x: T): Pick<T, Exclude<keyof T, "b">> & AB => <any>x

// Would really like this to be an interface extending T, not just an intersection type (although admittedly, this is awesome anyways, but that's not really the point here)
type X<T extends A = AA> = T & {yoho: number}

const before: X = {a: "a", b: "b", yoho: 10}
const after: X<AB> = converter(before)
const awesomeString = `${after.a} ${after.b_conv} ${after.yoho}`

Having the X type a first class interface would make this even cooler, although I'm just amazed by the new advanced types in TS lately. It's a new world of type programming!

yes, please.

@DanielRosenwasser I agree. For example:

interface Animal {
  numberOfLegs: number;
}

interface Male {
  yChromosome: any;
}

interface Female<A> extends A { //Error: An interface may only extend a class or another interface.
  mate(male:Male & A): (A & (Male | Female<A>))[];
}

Still gets an error of: An interface may only extend a class or another interface.

This is exactly my use-case (minus the precise example objects ;)) and my problem. +1 For this feature.

That will resolve my issues with : https://github.com/bterlson/strict-event-emitter-types

I will be able then to pass the T with Events to the StrictEventEmitter.

I'm not sure if this is still an open issue, but I found this after receiving the error when you make an interface extend a generic. In my case, I was trying to wrap Database results in a type which enumerates the extra keys added by the mysql library. What I ended up doing isn't in the discussion yet, so I thought I'd list it.

In my models file, I added

export interface DBInfo {
    insertId?: number;
    affectedRows?: number;
    changedRows?: number;
}

export type DBResponse<T> = DBInfo & T;

then, in other files, I can list the return type as DBResponse<User> and it attaches the extra keys correctly.

I use this small variant:

type Props<T = {}> = T & {
  id: string
}

type MyProps = {
  message: string
}

// Usage
class MyComponent {
  public props: Props<MyProps> = {
    id: 'ef6872a'
    message: 'Hola!'
  }
}

Since, the type declarations cannot return this from method calls. I think, having ability for interfaces to extend generics will be super helpful.

I would like to know more how interfaces extending generics is different and why is it more difficult than intersection?

interface BaseItem<T> extends T {
  title: string
  help?: string
}

type BaseItem<T> = T & {
  title: string
  help?: string
}

The issue with the type-instead-of-interface hack is that you cannot extend it for a class.

I think this feature is well-addressed by intersections.

@RyanCavanaugh This feature is _not_ adequately served by intersections, as @lazarljubenovic and others have pointed out in this thread, because types don't play well with classes. I'm consuming a JavaScript library that's using some "old school" JS techniques to achieve something similar to mixins/multiple inheritance, and without this feature I've not yet found a way for the type system to accurately reflect the shape of my classes. This forces me to discard the type system via any casts or @ts-ignore comments any time I need to access a class member that TypeScript is unaware of, which is not a sustainable solution for the long term. A generic interface that could inherit the members of its type argument would seamlessly and painlessly solve this issue, but instead I'm spending valuable hours struggling to hack the type system into giving me accurate feedback.

@dannymcgee I went through pretty much every example in this thread and confirmed that available alternates work, e.g. this is legal now (it didn't use to be)

type BaseItem<T> = T & {
  title: string
  help?: string
}

class M implements BaseItem<{x: string}> {
  x: string;
  title: string;
  help: string;
}

The OP is from 2015 and many interim comments refer scenarios that now work transparently or with minimal changes. I think a new issue that clearly outlines whatever shortcomings exist about current patterns and what would upsides could exist from U extends T (as compared to U & T in an alias) would be worthwhile.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MartynasZilinskas picture MartynasZilinskas  路  3Comments

remojansen picture remojansen  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

zhuravlikjb picture zhuravlikjb  路  3Comments

dlaberge picture dlaberge  路  3Comments