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

Suggestion: Interface extend generic type #2225

Closed
andreasbotsikas opened this issue Mar 6, 2015 · 37 comments
Closed

Suggestion: Interface extend generic type #2225

andreasbotsikas opened this issue Mar 6, 2015 · 37 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@andreasbotsikas
Copy link

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.

@pgrm
Copy link

pgrm commented May 7, 2015

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?

@richardhuaaa
Copy link

+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().

@eggers
Copy link

eggers commented Sep 4, 2015

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.

@danquirk danquirk added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Sep 4, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Sep 5, 2015

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

@pgrm
Copy link

pgrm commented Sep 5, 2015

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

@mhegazy
Copy link
Contributor

mhegazy commented Sep 6, 2015

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

@ronzeidman
Copy link

Will I be able to do something like this:

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

@DanielRosenwasser
Copy link
Member

@ronzeidman yes, that should work.

@pgrm
Copy link

pgrm commented Sep 12, 2015

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

@mhegazy
Copy link
Contributor

mhegazy commented Sep 12, 2015

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

@andreasbotsikas
Copy link
Author

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!

@DanielRosenwasser
Copy link
Member

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

@eggers
Copy link

eggers commented Sep 14, 2015

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

@JoshuaKGoldberg
Copy link
Contributor

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

@eggers
Copy link

eggers commented Sep 22, 2015

@JoshuaKGoldberg Good real world example.

@louy
Copy link

louy commented Dec 2, 2015

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.

@DmitryEfimenko
Copy link

+1

2 similar comments
@lseguin42
Copy link

+1

@Heartnett
Copy link

+1

@codedogfish
Copy link

@louy GREAT!!!

@nevir
Copy link

nevir commented Jan 5, 2017

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

@atrauzzi
Copy link

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.

@andraaspar
Copy link
Contributor

andraaspar commented Feb 9, 2017

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

@louy
Copy link

louy commented Feb 9, 2017

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

@louy
Copy link

louy commented Feb 9, 2017

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

@tvedtorama
Copy link

tvedtorama commented May 3, 2018

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!

@SalvatorePreviti
Copy link

yes, please.

@kael-shipman
Copy link

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

@TheAifam5
Copy link

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.

@swimmadude66
Copy link

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.

@joseluisq
Copy link

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!'
  }
}

@thetutlage
Copy link

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

@ackvf
Copy link

ackvf commented Feb 3, 2020

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
}

@lazarljubenovic
Copy link

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

@RyanCavanaugh
Copy link
Member

I think this feature is well-addressed by intersections.

@dannymcgee
Copy link

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

@RyanCavanaugh
Copy link
Member

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

@microsoft microsoft locked as resolved and limited conversation to collaborators Nov 30, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests