Skip to content

Suggestion: Type Property type #1295

Description

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 ?

Activity

  1. NoelAbrahams commented on Nov 28, 2014

    @NoelAbrahams

    Possible duplicate of #394

    See also #1003 (comment)

  2. fdecampredon commented on Nov 28, 2014

    @fdecampredon
    Author

    Noel Abrahams (@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

  3. s-panferov commented on Nov 29, 2014

    @s-panferov

    François de Campredon (@fdecampredon)

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

    What to do in this case?

  4. fdecampredon commented on Nov 29, 2014

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

  5. Igorbek commented on Dec 1, 2014

    @Igorbek
    Contributor

    Great proposal. Agree, it would be useful feature.

  6. NoelAbrahams commented on Dec 1, 2014

    @NoelAbrahams

    François de Campredon (@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.

  7. Igorbek commented on Dec 1, 2014

    @Igorbek
    Contributor

    Noel Abrahams (@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.

  8. RyanCavanaugh commented on Dec 1, 2014

    @RyanCavanaugh
    Member

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

  9. NoelAbrahams commented on Dec 1, 2014

    @NoelAbrahams

    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.

  10. spion commented on Dec 2, 2014

    @spion

    #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>;
      }
    }
  11. NoelAbrahams commented on Dec 2, 2014

    @NoelAbrahams

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

  12. fdecampredon commented on Dec 2, 2014

    @fdecampredon
    Author

    OK Noel Abrahams (@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 ;).

  13. NoelAbrahams commented on Dec 2, 2014

    @NoelAbrahams
  14. 61 remaining items

  15. tinganho commented on Aug 30, 2016

    @tinganho
    Contributor

    Daniel Rosenwasser (@DanielRosenwasser) Having barely went down the rabbit hole. I still can't find any problems with implementing Kagami Sascha Rosylight (@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].

  16. Igorbek commented on Sep 2, 2016

    @Igorbek
    Contributor

    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 ?

    Daniel Rosenwasser (@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 {
        // ...
    }
  17. rtm commented on Sep 12, 2016

    @rtm

    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;
    
  18. tinganho commented on Sep 12, 2016

    @tinganho
    Contributor

    Bob Myers (@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.

  19. modified the milestones: TypeScript 2.1, , on Oct 25, 2016
  20. ahejlsberg commented on Nov 2, 2016

    @ahejlsberg
    Member

    Implementation now available in #11929.

  21. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

FixedA PR has been merged for this issueHelp WantedYou can do thisSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions