Suggestion: Type Property type
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
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 ?