-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
Possible duplicate of #394 See also #1003 (comment) |
@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 |
contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13) What to do in this case? |
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 |
Great proposal. Agree, it would be useful feature. |
@fdecampredon, I do believe this is a duplicate. See the comment from Dan and the corresponding response which contains the suggestion for IMO all this is a lot of new syntax for a rather narrow use-case. |
@NoelAbrahams it's not the same.
There's a brifge to 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 |
This is interesting and desirable. TypeScript doesn't do well with string-heavy frameworks yet and this would obviously help a lot. |
True. Still doesn't change the fact that this is a duplicate suggestion.
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. |
#1003 suggests declare module ImmutableJS {
class Map<T> {
get(prop: memberof T): T[prop];
set(prop: memberof T, value: T[prop]): Map<T>;
}
} |
@spion, did you mean #394? If you were to read down further you would see the following:
|
OK @NoelAbrahams there was a comment in #394 that was trying to describe more or less the same thing that this one. |
@fdecampredon, more the merrier 😃 |
@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) |
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 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 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 The idea is to somewhat generate this type from a type parameter. We might define some special dummy generic types 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. |
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 |
Note that next version of react would greatly benefit of that approach, see facebook/react#3398 |
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)? |
Approved. We will want to try this out in an experimental branch to get a feel for the syntax. |
Just wondering which version of the proposal is going to be implemented? I.e. is |
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? |
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. |
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>! |
Regarding the name, I started calling this feature (at least the form that @Artazor proposed) "Indexed Generics" |
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:
|
@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:
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, 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 Hope that gives you all an update. |
@DanielRosenwasser Thanks for the update. I just saw @weswigham submitted a PR about the I just wonder why you decided to depart from the original proposed syntax? function get(prop: string): T[prop]; and introduce |
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. |
@DanielRosenwasser Having barely went down the rabbit hole. I still can't find any problems with implementing @saschanaz idea? I think My rough implementation thought was to introduce a new type called export interface PropertyReferencedType extends Type {
property: Symbol;
targetType: ObjectType;
} When entering a function declared with a return type that is of 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 The new type interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string A |
If you introduce 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 {
// ...
} |
I am not seeing how the signature would be written for Underscore's
|
@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. |
Implementation now available in #11929. |
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.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 :
For the case of
pluck
, it's a transformationwhere 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]
whereprop
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 :
This way, when we use our Backbone model, TypeScript could correctly type-check the
get
andset
call.The
prop
constantConstraint
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:If
T
is indexable, our map inherit of this behavior:Now
get
has for typeget(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 :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 typeT[prop]
, shoudT[prop]
be resolved to{}
orany
, if so should the compiler with--noImplicitAny
raise an error ?The text was updated successfully, but these errors were encountered: