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

Mapped instanceof class type #13009

Closed
tinganho opened this issue Dec 18, 2016 · 13 comments
Closed

Mapped instanceof class type #13009

tinganho opened this issue Dec 18, 2016 · 13 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@tinganho
Copy link
Contributor

tinganho commented Dec 18, 2016

Some API in pure JS accepts classes declarations as input and generate instances of that class as output.

Input => class declaration
Output => instance of the class declaration

As of know, to my knowledge, there is no way to map that, if the input argument accepts an index signature of class declarations, such as below:

interface Relations {
    [index: string]: new (...args: any[]) => Model<any, any>
}

It would be great if I could map from class type to instance type in those cases.

type Instance<T> = {
   [P in keyof T]: instanceof T[P]
}

Where the instanceof operator just get the instance side of a class. So the below is equivalent:

class A {};
let a: A;
let a: instanceof A;

This is my real use case, a project I'm working on where I try to design some data classes. Where I do relational mapping for model objects:

type Instance<T> = {
   [P in keyof T]: instanceof T[P]
}

interface Configurations<R extends Relations> {
    relations: R;
}

class Model<P, R>{
    static define<P, R extends Relations>(configurations?: Configurations<R>) {
        return class extends Model<P, R> {

        }
    }
    props: P & Instance<R>;


    /**
     * Get property value.
     */
    public get<K extends keyof (P & Instance<R>)>(prop: K): (P & Instance<R>)[K] | null {
        return typeof this.props[prop] !== 'undefined' ? this.props[prop] : null;
    }
}

interface Relations {
    [index: string]: new (...args: any[]) => Model<any, any>
}

interface IUser {
    name: string;
}

const User = Model.define<IUser, {}>();

interface IPost {
    name: string;
}

const relations = {
    owner: User,
};

const Post = Model.define<IPost, typeof relations>({
    relations: relations,
});

const post = new Post();
post.get('name');
const p = post.get('owner'); // User, and not typeof User

One alternative right now, is to pass in the type manually, which is quite tedious:

interface Relations {
   owner: User;
}
const relations = {
    owner: User,
};
const Post = Model.define<IPost, Relations, typeof relations>({
    relations: relations,
});
@tinganho tinganho changed the title Mapped instanceof class Mapped instanceof class type Dec 18, 2016
@DanielRosenwasser
Copy link
Member

The problem is that this gets messy if you have multiple construct signatures, or a possibly different prototype property.

Though all in all, I want this for grabbing the return type of a function types in general (types with call or construct signatures).

#6606 is vaguely related.

@kimamula
Copy link
Contributor

kimamula commented Dec 30, 2016

I think your requirement can be filled with the following implementation.

type Constructor<T> = {
    [P in keyof T]: { new (...args: any[]): T[P]; };
}

interface Configurations<R> {
    relations: Constructor<R>;
}

class Model<P, R>{
    static define<P, R>(configurations?: Configurations<R>) {
        return class extends Model<P, R> {

        }
    }
    props: P & R;


    /**
     * Get property value.
     */
    public get<K extends keyof P>(prop: K): P[K] | null;
    public get<K extends keyof R>(prop: K): R[K] | null;
    public get(prop: any): any {
        return typeof this.props[prop] !== 'undefined' ? this.props[prop] : null;
    }
}

interface IUser {
    name: string;
}

const User = Model.define<IUser, {}>();

interface IPost {
    name: string;
}

const relations = {
    owner: User,
};

const Post = Model.define<IPost, { owner: Model<IUser, {}> }>({
    relations: relations,
});

const post = new Post();
post.get('name');
const p = post.get('owner'); // User, and not typeof User

@tinganho
Copy link
Contributor Author

tinganho commented Dec 30, 2016

@kimamula your solution doesn't solve the problem. You still declare relations two times in:

const relations = {
    owner: User,
};

const Post = Model.define<IPost, { owner: Model<IUser, {}> }>({
    relations: relations,
});

@kimamula
Copy link
Contributor

@tinganho you can also write as follows in my example.

const Post = Model.define<IPost, { owner: Model<IUser, {}> }>({
    relations: {
        owner: User,
    },
});

Or I might be misunderstanding your problem.
<IPost, { owner: Model<IUser, {}> }> part cannot be omitted as TypeScript cannot infer IPost type from the usage.

@tinganho
Copy link
Contributor Author

tinganho commented Dec 30, 2016

Though all in all, I want this for grabbing the return type of a function types in general (types with call or construct signatures).

You mean something like typeof new T[P](string, number)? That's probably a better solution 👍

@kimamula
Copy link
Contributor

kimamula commented Dec 30, 2016

Well, though it is not possible to grab the return types of functions, I suppose something like the following would work in most of the cases, as I wrote above.

type Functions<FunctionReturnTypes> = {
    [P in keyof FunctionReturnTypes]: () => FunctionReturnTypes[P]
}

@tinganho
Copy link
Contributor Author

@kimamula these are suggestions on solutions to problems that are supposably not solvable today.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 30, 2016

Why can not you just write your generic types to capture the instance; instead of capturing the class, and then needing to get the instance out of it.. e.g.:

type Class<T> = {
  new (...args: any[]): T;
};

type Map<T> = {
  [x: string]: T;
}

type Relations<T> = Map<Class<T>>;

interface Configurations<T> {
    relations: Relations<T>;
}

class Model<T>{
    static define<T>(configurations?: Configurations<Model<T>>) {
        return class extends Model<T> {

        }
    }
    props: T;


    /**
     * Get property value.
     */
    public get<K extends keyof (T)>(prop: K): (T)[K] | null {
        return typeof this.props[prop] !== 'undefined' ? this.props[prop] : null;
    }
}

interface IUser {
    name: string;
}

const User = Model.define<IUser>();

interface IPost {
    name: string;
}

const relations = {
    owner: User,
};

const Post = Model.define({
    relations: relations,
});

const post = new Post();

post.get('name'); // this works now
const p = post.get('owner'); // this does not

@tinganho
Copy link
Contributor Author

tinganho commented Dec 30, 2016

@mhegazy I think you probably misunderstood my case. Relations can have multiple different classes and not just one class:

const relations = {
    owner1: User,
    comment: Comment,
};

Your example does not seem to reflect that? Since Map and Class only capture one type. Thus I need Mapped Types.

And I want post.get('owner') to have the type User:

const p = post.get('owner'); // User

But remember, I don't want declare relations twice, i.e. one for the type and one for the class value.

There are a few MVC libs that does something similar with relational mapping.
http://backbonerelational.org/#relations-relatedModel
https://guides.emberjs.com/v2.10.0/models/relationships/

Though, none achieve this with static types.

@kimamula
Copy link
Contributor

kimamula commented Dec 30, 2016

But remember, I don't want declare relations twice, i.e. one for the type and one for the class value.

I really cannot understand why you think relations is "declared twice" in my code.
As I wrote, <IPost, { owner: Model<IUser, {}> }> is necessary not for { owner: Model<IUser, {}> } but for IPost, i.e., it is required to compile post.get('name');, not post.get('owner');.
TypeScript can infer { owner: Model<IUser, {}> } type correctly without it.

@kimamula
Copy link
Contributor

kimamula commented Dec 31, 2016

The essential problem here may be that TypeScript requires all type parameters if it cannot infer any of them.
Then, a solution should be something like this.

const Post = Model.define<IPost, >({ // inferable type parameter can be omitted
    relations: {
        owner: User,
    },
});

I actually sometimes feel I need this feature.

EDIT: There is already an issue for this feature.
#10571

@mhegazy
Copy link
Contributor

mhegazy commented Jan 4, 2017

@mhegazy I think you probably misunderstood my case. Relations can have multiple different classes and not just one class:

if Relations is defined as a mapped type, what do you expect to infer from it, e.g.:

const relations = {
    owner1: User,
    comment: Comment,
};

What is is T you are looking out of this? is it IUser | IComponet? and how would that help you in the return value of define?

I would say you want IUser & IComponent, and you want define to return something that extends Model<IUser & IComponent>.but there is no reduce operator on mapped types that can get you the intersection type of the types of mapped type.

@mhegazy mhegazy added the Needs More Info The issue still hasn't been fully clarified label Jan 4, 2017
@tinganho
Copy link
Contributor Author

tinganho commented Jan 7, 2017

What is is T you are looking out of this? is it IUser | IComponet

Why does T need to resolve to a type here? In my original post Relations is not a generic.

@mhegazy mhegazy closed this as completed Apr 21, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

4 participants