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 ?