GeezerVison is showing github.com — Suggestion: Type Property type · Issue #1295 · microsoft/TypeScript
Suggestion: Type Property type #1295DescriptionMotivations
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 ? Reacted by Zak Henry, Charles Samborski, Tingan Ho, Tom Duncalf, Michael Messer, Caleb Meredith, Steve Sewell, Laurence Dougal Myers, Kagami Sascha Rosylight, Michał Stocki and 19 more Reacted by Louay Akkad, Michael Messer, Caleb Meredith, Steve Sewell, Artur Eshenbrener, Alex, Michael Konviser, normalser, Scott Cheng and Dan Kirkham ActivityNoelAbrahams commented on Nov 28, 2014 fdecampredon commented on Nov 28, 2014 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 Reacted by Michał Stocki, bryanrideshark, Shannon V.C. and olyis Reacted by bryanrideshark s-panferov commented on Nov 29, 2014 fdecampredon commented on Nov 29, 2014 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 commented on Dec 1, 2014 Great proposal. Agree, it would be useful feature. NoelAbrahams commented on Dec 1, 2014 Igorbek commented on Dec 1, 2014 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. RyanCavanaugh commented on Dec 1, 2014 This is interesting and desirable. TypeScript doesn't do well with string-heavy frameworks yet and this would obviously help a lot. Reacted by Louay Akkad, Michael Messer, Ethan Resnick and olyis NoelAbrahams commented on Dec 1, 2014
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 commented on 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 commented on Dec 2, 2014 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.
fdecampredon commented on Dec 2, 2014 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 ;). NoelAbrahams commented on Dec 2, 2014 tinganho commented on Aug 30, 2016 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]. Reacted by Kagami Sascha Rosylight, Herrington Darkholme and Alex Igorbek commented on 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 ?
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 {
// ...
}rtm commented on 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 commented on Sep 12, 2016 Reacted by Kagami Sascha Rosylight ahejlsberg commented on Nov 2, 2016 Implementation now available in #11929. Reacted by Ryan Cavanaugh, Bazyli Brzóska, Ali Taheri Moghaddar, Jari Pennanen, spion and Marin Marinov
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|