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: Type Property type #1295

Closed
fdecampredon opened this issue Nov 28, 2014 · 76 comments
Closed

Suggestion: Type Property type #1295

fdecampredon opened this issue Nov 28, 2014 · 76 comments
Assignees
Labels
Fixed A PR has been merged for this issue Help Wanted You can do this Suggestion An idea for TypeScript

Comments

@fdecampredon
Copy link

Motivations

A lot of JavaScript library/framework/pattern involve computation based on the property name of an object. For example Backbone model, functional transformation pluck, ImmutableJS are all based on such mechanism.

//backbone
var Contact = Backbone.Model.extend({})
var contact = new Contact();
contact.get('name');
contact.set('age', 21);

// ImmutableJS
var map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

//pluck
var arr = [{ name: 'François' }, { name: 'Fabien' }];
_.pluck(arr, 'name') // ['François', 'Fabien'];

We can easily understand in those examples the relation between the api and the underlying type constraint.
In the case of the backbone model, it is just a kind of proxy for an object of type :

interface Contact {
  name: string;
  age: number;
}

For the case of pluck, it's a transformation

T[] => U[]

where U is the type of a property of T prop.

However we have no way to express such relation in TypeScript, and ends up with dynamic type.

Proposed solution

The proposed solution is to introduce a new syntax for type T[prop] where prop is an argument of the function using such type as return value or type parameter.
With this new type syntax we could write the following definition :

declare module Backbone {

  class Model<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): void;
  }
}

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): Map<T>;
  }
}

declare function pluck<T>(arr: T[], prop: string): Array<T[prop]>  // or T[prop][] 

This way, when we use our Backbone model, TypeScript could correctly type-check the get and set call.

interface Contact {
  name: string;
  age: number;
}
var contact: Backbone.Model<Contact>;

var age = contact.get('age');
contact.set('name', 3) /// error

The prop constant

Constraint

Obviously the constant must be of a type that can be used as index type (string, number, Symbol).

Case of indexable

Let's give a look at our Map definition:

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[string];
    set(prop: string, value: T[string]): Map<T>;
  }
}

If T is indexable, our map inherit of this behavior:

var map = new ImmutableJS.Map<{ [index: string]: number}>;

Now get has for type get(prop: string): number.

Interrogation

Now There is some cases where I have pain to think of a correct behavior, let's start again with our Map definition.
If instead of passing { [index: string]: number } as type parameter we would have given
{ [index: number]: number } should the compiler raise an error ?

if we use pluck with a dynamic expression for prop instead of a constant :

var contactArray: Contact[] = []
function pluckContactArray(prop: string) {
  return _.pluck(myArray, prop);
}

or with a constant that is not a property of the type passed as parameter.
should the call to pluck raise an error since the compiler cannot infer the type T[prop], shoud T[prop] be resolved to {} or any, if so should the compiler with --noImplicitAny raise an error ?

@NoelAbrahams
Copy link

Possible duplicate of #394

See also #1003 (comment)

@fdecampredon
Copy link
Author

@NoelAbrahams I really don't think that it's a duplicate of #394, on the contrary both features are pretty complementary something like :

 class Model<T> {
    get(prop: memberof T): T[prop];
    set(prop:  memberof T, value: T[prop]): void;
  }

Would be ideal

@s-panferov
Copy link

@fdecampredon

contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13)

What to do in this case?

@fdecampredon
Copy link
Author

It's more or less the same case than the one from the last paragraph of my issue. Like I said we have multiple choice, We can report an error, or infer any for T[prop], I think the second solution is more logical

@Igorbek
Copy link
Contributor

Igorbek commented Dec 1, 2014

Great proposal. Agree, it would be useful feature.

@NoelAbrahams
Copy link

@fdecampredon, I do believe this is a duplicate. See the comment from Dan and the corresponding response which contains the suggestion for membertypeof.

IMO all this is a lot of new syntax for a rather narrow use-case.

@Igorbek
Copy link
Contributor

Igorbek commented Dec 1, 2014

@NoelAbrahams it's not the same.

  • memberof T returns type which instance could only be a string with valid property name of T instance.
  • T[prop] returns type of the property of T named with string which is represented by prop argument/variable.

There's a brifge to memberof that type of prop parameter should be memberof T.

Actually, I would like to have more rich system for type inference based on type metadata. But such operator is a good start as well as memberof.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Dec 1, 2014
@RyanCavanaugh
Copy link
Member

This is interesting and desirable. TypeScript doesn't do well with string-heavy frameworks yet and this would obviously help a lot.

@NoelAbrahams
Copy link

TypeScript doesn't do well with string-heavy frameworks

True. Still doesn't change the fact that this is a duplicate suggestion.

yet and this would obviously help a lot [to typing string-heavy frameworks]

Not sure about that. Seems rather piecemeal and somewhat specific to the proxy-object pattern outlined above. I would much prefer a more holistic approach to the magic string problem along the lines of #1003.

@spion
Copy link

spion commented Dec 2, 2014

#1003 suggests any as the return type of a getter. This proposal adds on top that by adding a way to look up the value type too - the mix would result in something like this:

declare module ImmutableJS {
  class Map<T> {
    get(prop: memberof T): T[prop];
    set(prop: memberof T, value: T[prop]): Map<T>;
  }
}

@NoelAbrahams
Copy link

@spion, did you mean #394? If you were to read down further you would see the following:

I thought about the return type but left it out as to not make the overall suggestion too big of a bite.

This was my initial thought but has problems. What if there are multiple arguments of type memberof T, which one does membertypeof T refer to?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

This solves the "which argument am I referring to" problem, but the membertypeof name seems wrong and not a fan of the operator targeting the property name.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

I think this works better.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

Unfortunately not sure that I have a great solution although I believe the last suggestion has decent potential.

@fdecampredon
Copy link
Author

OK @NoelAbrahams there was a comment in #394 that was trying to describe more or less the same thing that this one.
Now I think than T[prop] is perhaps a little more elegant than the different propositions of this comment, and that the proposition in this issue goes perhaps a little further in the reflection.
For theses reason I don't think that it should be closed as a duplicate.
But I guess I'm biased since I'm the one who wrote the issue ;).

@NoelAbrahams
Copy link

@fdecampredon, more the merrier 😃

@spion
Copy link

spion commented Dec 2, 2014

@NoelAbrahams oops, I missed that part. Sure, those are pretty much equivalent (this one doesn't seem to introduce another generic parameter, which may or may not be a problem)

@Nevor
Copy link

Nevor commented Dec 3, 2014

Having taken a look at Flow, I think it would be more elegant with a little stronger type system and special types rather than an ad hoc type narrowing.

What we means with get(prop: string): Contact[prop] for instance is just a series of possible overloading :

interface Map {
  get(prop : string) : Contact[prop];
}

// is morally equivalent to 

interface Map {
  get(prop : "name") : string;
  get(prop : "age") : number;
}

Assuming the existence of the & type operator (intersection types), this is type equivalent to

interface Map {
   get : (prop : "name") => string & (prop : "age") => number;
}

Now that we have translated our non-generic case in only types expression with no special treatment (no [prop]), we can address the question of parameters.

The idea is to somewhat generate this type from a type parameter. We might define some special dummy generic types $MapProperties, $Name and $Value to express our generated type in term of only type expression (no special syntax) while still hinting the type checker that something should be done.

class Map<T> {
   get : $MapProperties<T, (prop : $Name) => $Value>
   set : $MapProperties<T, (prop : $Name, val : $Value) => void>
}

It might seem complicated and near templating or poor's man dependent types but it cannot be avoided when someone want types to depend upon values.

@grncdr
Copy link

grncdr commented Mar 17, 2015

Another area where this would be useful is iterating over the properties of a typed object:

interface Env {
 // pretend this is an actually interesting type
};

var actions = {
  action1: function (env: Env, x: number) : void {},
  action2: function (env: Env, y: string) : void {}
};

// actions has type { action1: (Env, number) => void; action2: (Env, string) => void; }
var env : Env = {};
var boundActions = {};
for (var action in actions) {
  boundActions[action] = actions[action].bind(null, env);
}

// boundActions should have type { action1: (number) => void; action2: (string) => void; }

These types should be at least theoretically possible to infer (there's enough type information to infer the result of the for loop), but it's also probably quite a stretch.

@fdecampredon
Copy link
Author

Note that next version of react would greatly benefit of that approach, see facebook/react#3398

@qc00
Copy link

qc00 commented Mar 24, 2015

Like #1295 (comment), when the string is supplied by an expression other than a string literal this feature quickly breaks down due to the Halting Problem. However, can a basic version of this still be implemented using the learning from ES6 Symbol problem (#2012)?

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Apr 27, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Apr 27, 2015
@RyanCavanaugh
Copy link
Member

Approved.

We will want to try this out in an experimental branch to get a feel for the syntax.

@qc00
Copy link

qc00 commented Apr 28, 2015

Just wondering which version of the proposal is going to be implemented? I.e. is T[prop] going to be evaluated at the call site and eagerly replaced with a concrete type or is it going to become a new form of type variable?

@Lenne231
Copy link

Lenne231 commented Aug 2, 2015

I think we should rely on a more general and less verbose syntax as defined in #3779.

interface Map<T> {
  get<A>(prop: $Member<T,A>): A;
  set<A>(prop: $Member<T,A>, value: A): Map<T>;
}

Or is it not possibe to infer the type of A?

@s-panferov
Copy link

Just want to say that I made a little codegen tool to make TS integration with ImmutableJS easier while we are waiting for the normal solution: https://www.npmjs.com/package/tsimmutable. It is quite simple, but I think it will work for most use cases. Maybe it will help someone.

@s-panferov
Copy link

Also I want to note, that the solution with a member type may not work with ImmutableJS:

interface Profile {
  firstName: string 
}

interface User {
  profile: Profile  
}

let a: Map<User> = fromJS(/* ... */);
a.get('profile') // Type will be Profile, but the real type is Map<Profile>!

@spion
Copy link

spion commented Aug 18, 2016

Regarding the name, I started calling this feature (at least the form that @Artazor proposed) "Indexed Generics"

@Igorbek
Copy link
Contributor

Igorbek commented Aug 18, 2016

A solution from another angle of view could be for this problem. I'm not sure if it's been brought already, it's a long thread. Developing a string generic suggestion, we could extend indexation signature. Since string literals can be used for indexer type, we could have these to be equivalent (as I know they're not at the moment):

interface A1 {
    a: number;
    b: boolean;
}
interface A2 {
    [index: "a"]: number;
    [index: "b"]: boolean;
}

So, we could just write then

declare function pluck<P, T extends { [indexer: P]: R; }, R>(obj: T, p: P): R;

There're a few things need to consider:

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Aug 18, 2016

@weswigham @mhegazy, and I have been discussing this recently; we'll let you know any developments we run into, and keep in mind this is just having prototyped the idea.

Current ideas:

  • A keysof Foo operator to grab the union of property names from Foo as string literal types.
  • A Foo[K] type which specifies that for some type K that is a string literal types or union of string literal types.

From these basic blocks, if you need to infer a string literal as the appropriate type, you can write

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

Here, K will be a subtype of keysof T which means that it is a string literal type or a union of string literal types. Anything you pass in for the key parameter should be contextually typed by that literal/union of literals, and inferred as a singleton string literal type.

For instance

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

Biggest issue is that keysof often needs to "delay" its operation. It is too eager in how it evaluates a type, which is a problem for type parameters like in the first example I posted (i.e. the case we really want to solve is actually the hard part 😄).

Hope that gives you all an update.

@tinganho
Copy link
Contributor

tinganho commented Aug 20, 2016

@DanielRosenwasser Thanks for the update. I just saw @weswigham submitted a PR about the keysof operator, so it is maybe better to hand this issue off to you guys.

I just wonder why you decided to depart from the original proposed syntax?

function get(prop: string): T[prop];

and introduce keysof?

@DanielRosenwasser
Copy link
Member

T[prop] is less general, and requires a lot of interlaced machinery. One big question here is how you'd even relate the literal contents of prop to property names of T. I'm not even completely sure what you'd do. Would you add an implicit type parameter? Would you need to change contextual typing behavior? Would you need to add something special to signatures?

The answer is probably yes to all of those things. I drove us away from that because my gut told me it was better to use two simpler, separate concepts and build up from there. The downside is there is a little bit more boilerplate in certain cases.

If there are newer libraries that uses these sorts of patterns and that boilerplate is making it hard for them to write in TypeScript, then maybe we should consider that. But overall this feature is primarily meant to serve library consumers, because the use-site is where you get the benefits here anyway.

@tinganho
Copy link
Contributor

tinganho commented Aug 30, 2016

@DanielRosenwasser Having barely went down the rabbit hole. I still can't find any problems with implementing @saschanaz idea? I think keysof is redundant in this case. T[p] already relates that p must be one of the literal props of T.

My rough implementation thought was to introduce a new type called PropertyReferencedType.

export interface PropertyReferencedType extends Type {
        property: Symbol;
        targetType: ObjectType;
}

When entering a function declared with a return type that is of PropertyReferencedType or entering a function that references PropertyReferencedType: A type of a ElementAccessExpression will be augmented with a property that references the symbol of the accessed property.

export interface Type {
        flags: TypeFlags;                // Flags
        /* @internal */ id: number;      // Unique ID
        //...
        referencedProperty: Symbol; // referenced property
}

So a type with a referenced property symbol is assignable to a PropertyReferencedType. During checking, the referencedProperty must correspond to p in T[p]. Also the parent type of a element access expression must be assignable to T. And to make things easier p must also be const.

The new typePropertyReferencedType only exists inside the function as an "unresolved type". On call site one have to resolve the type with p:

interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string

A PropertyReferencedType only propagates through function assignments and cannot propagate through call expressions, because a PropertyReferencedType is only a temporary type meant to help with checking the body of a function with return type T[p].

@Igorbek
Copy link
Contributor

Igorbek commented Sep 2, 2016

If you introduce keysof and T[K] type operators, would it mean we could use them like this:

interface A {
  a: number;
  b: string;
}
type AK = keysof A; // "a" | "b"
type AV = A[AK]; // number | string ?
type AA = A["a"]; // number ?
type AB = A["b"]; // string ?
type AC = A["c"]; // error?
type AN = A[number]; // error?

type X1 = keysof { [index: string]: number; }; // string ?
type X2 = keysof { [index: string]: number; [index: number]: string; }; // string | number ?

@DanielRosenwasser wouldn't your example have the same meaning with my

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}
// same as ?
function foo<K, V, T extends { [k: K]: V; }>(obj: T, key: K): V {
    // ...
}

@rtm
Copy link

rtm commented Sep 12, 2016

I am not seeing how the signature would be written for Underscore's _.pick:

o2 = _.pick(o1, 'p1', 'p2');

pick(Object, ...props: String[]) : WHAT GOES HERE;

@tinganho
Copy link
Contributor

@rtm I suggested it in #1295 (comment). Though it might be better to open a new issue, even though it is related to this one.

@ahejlsberg
Copy link
Member

Implementation now available in #11929.

@mhegazy mhegazy added the Fixed A PR has been merged for this issue label Nov 2, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests