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.
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
prop
constantObviously the constant must be of a type that can be used as index type (string
, number
, Symbol
).
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
.
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 ?
Possible duplicate of #394
See also https://github.com/Microsoft/TypeScript/issues/1003#issuecomment-61171048
@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
@fdecampredon
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 any
for T[prop]
, I think the second solution is more logical
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 membertypeof
.
IMO all this is a lot of new syntax for a rather narrow use-case.
@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
.
This is interesting and desirable. TypeScript doesn't do well with string-heavy frameworks yet and this would obviously help a lot.
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.
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>;
}
}
@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 doesmembertypeof 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.
OK @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 ;).
@fdecampredon, more the merrier :smiley:
@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 get(prop: string): Contact[prop]
for instance is just a series of possible overloading :
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 &
type operator (intersection types), this is type equivalent to
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 [prop]
), we can address the question of parameters.
The idea is to somewhat generate this type from a type parameter. We might define some special dummy generic types $MapProperties
, $Name
and $Value
to express our generated type in term of only type expression (no special syntax) while still hinting the type checker that something should be done.
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 for
loop), but it's also probably quite a stretch.
Note that next version of react would greatly benefit of that approach, see https://github.com/facebook/react/issues/3398
Like https://github.com/Microsoft/TypeScript/issues/1295#issuecomment-64944856, 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 T[prop]
going to be evaluated at the call site and eagerly replaced with a concrete type or is it going to become a new form of type variable?
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>!
@s-panferov Something like this could work:
interface ImmutableMap<T> {
get<A extends boolean | number | string>(key : string) : A;
get<A extends {}>(key : string) : ImmutableMap<A>;
get<E, A extends Array<any>>(key : string) : ImmutableList<E>;
}
interface Profile {
}
interface User {
name : string;
profile : Profile;
}
var map : ImmutableMap<User>;
var name = map.get<string>('name'); // string
var profile = map.get<Profile>('profile'); // ImmutableMap<Profile>
This won't exclude DOM nodes or Date objects but it shouldn't be allowed to use them in immutable structures at all. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects
I think it is useful to first start with the best currently available workaround, and then gradually work it out from there.
Let's say we need a function that returns the value of a property for any containing non-primitive object:
function getProperty<T extends object>(container: T; propertyName: string) {
return container[propertyName];
}
Now we want the return value to have the type of the target property, so another generic type parameter could be added for its type:
function getProperty<T extends object, P>(container: T; propertyName: string) {
return <P> container[propertyName];
}
So a use case with a class would look like:
class C {
member: number;
static member: string;
}
let instance = new C();
let result = getProperty<C, typeof instance.member>(instance, "member");
And result
would correctly receive the type number
.
However, there seems to be a duplicate reference to 'member' in the call: one is in the type parameter, and the other one is a _literal_ string that will always receive the string representation of the target property name. The idea of this proposal is that these two can be unified to a single type parameter that would only receive the string representation.
So it needs to be observed here that the string would also act as a _generic parameter_, and will need be passed as a literal for this to work (other cases could silently ignore its value unless some form of runtime reflection is available). So to make the semantics clear, there needs to be some way to signify a relation between the generic parameter and the string:
function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
return <T[PName]> container[propertyName];
}
And now the calls would look like:
let instance = new C();
let result = getProperty<C>(instance, "member");
Which would internally resolve to:
let result = getProperty<C, "member">(instance, "member");
However since C
contains both an instance and static property named member
, the higher-kind generic expression T[PName]
is ambiguous. Since the intention here is mostly to apply it to properties of instances, a solution like the proposed typeon
operator could be used internally, which may also improve the semantics as T[PName]
is interpreted by some as representing a _value reference_, not necessarily a type:
function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
return <typeon T[PName]> container[propertyName];
}
(This would also work if T
is a compatible interface type, as typeon
supports interfaces as well)
To get the static property, the function would be called with the constructor itself and the constructor type should be passed as typeof C
:
let result = getProperty<typeof C>(C, "member"); // Note it is called with the constructor object
(Internally typeon (typeof C)
resolves to typeof C
so this would work)
result
will now correctly receive the type string
.
To support additional types of property identifiers, this can be rewritten as:
type PropertyIdentifier = string|number|Symbol;
function getProperty<T extends object, PName: PropertyIdentifier = propertyName>(container: T; propertyName: PropertyIdentifier) {
return <typeon T[PName]> container[propertyName];
}
So there are several different features that are needed to support this:
Now let's get back to the original workaround:
function getProperty<T extends object, P>(container: T; propertyName: string) {
return <P> container[propertyName];
}
There is one advantage about this workaround, is that it can be easily extended to support more complex path names, referencing not only direct properties, but properties of nested objects:
function getPath<T extends object, P>(container: T; path: string) {
... more complex code here ...
return <P> resultValue;
}
and now:
class C {
a: {
b: {
c: string[];
}
}
}
let instance = new C();
let result = getPath<C, typeof instance.a.b.c>(instance, "a.b.c");
result
would correctly get the type string[]
here.
The question is, should nested paths be supported as well? Will the feature be really that useful if no auto-completion is available when typing them?
This suggests that a different solution might be possible, which would work in the other direction. Back to the original workaround:
function getProperty<T extends object, P>(container: T; propertyName: string) {
return <P> container[propertyName];
}
Looking at this from the other direction, it is possible to take the type reference itself and convert it to a string:
function getProperty<T extends object, P>(container: T; propertyName: string = @typeReferencePathOf(P)) {
return <P> container[propertyName];
}
@typeReferencePathOf
is similar in concept to nameOf
but applies to generic parameters. It would take the received type reference typeof instance.member
(or alternatively typeon C.member
), extract the property path member
, and convert it to the literal string "member"
at compile time. A less specialized complementary @typeReferenceOf
would resolve to the complete string "typeof instance.member"
.
So now:
getProperty<C, typeof instance.subObject>(instance);
Would resolve to:
getProperty<C, typeof instance.subObject>(instance, "subObject");
And a similar implementation for getPath
:
getPath<C, typeof instance.a.b.c>(instance);
Would resolve to:
getPath<C, typeof instance.a.b.c>(instance, "a.b.c");
Since @typeReferencePathOf(P)
is only set as a default value. The argument can be stated manually if needed:
getPath<C, SomeTypeWhichIsNotAPath>(instance, "member.someSubMember.AnotherSubmember.data");
If the second type parameter would not be provided @typeReferencePathOf()
could resolve to undefined
or alternatively the empty string ""
. If a type was provided but did not have an internal path, it would resolve to ""
.
Advantages of this approach:
typeof
expression and uses an existing mechanism of type checking to validate it instead of needing to convert it from a string representation at compile-time.typeon
(but could support it).+1
Nice! I've already started to write a big proposal to address the same problem, but found this discussion, so let's discuss here.
Here is an extract from my not-submitted issue:
Question: How the following function should be declared in .d.ts to be usable in contexts where it is supposed to be used?
function mapValues(obj, fn) {
return Object.keys(obj)
.map(key => ({key, value: fn(obj[key], key)}))
.reduce((res, {key, value}) => (res[key] = value, res), {})
}
This function (with slight variations) could be found in almost every generic "utils" library.
There are two distinct use cases of the mapValues
in the wild:
a. _Object-as-a-__Dictionary_ where property names are dynamic, values has the same one type, and the function is in general case monomorphic (aware of this type)
var obj = {'a': 1, 'b': 2, 'c': 3};
var res = mapValues(x, val => val * 5); // {a: 5, b: 10, c: 15}
console.log(res['a']) // 5
b. _Object-as-a-__Record_ where each property has its own type, while the function is parametrically polymorphic (by implementation)
var obj = {a: 123, b: "Hello", c: true};
var res = mapValues(p, val => [val]); // {a: [123], b: ["Hello"], c: [true]}
console.log(res.a[0].toFixed(2)) // "123.00"
The best available instrumentation for the mapValues
with current TypeScript is:
declare function mapValues<T1, T2>(
obj: {[key: string]: T1},
fn: (arg: T1, key: string) => T2
): {[key: string]: T2};
It's not hard to see that this signature perfectly matches to the _Object-as-a-__Dictionary_ use-case, while produces a somewhat surprising1 result of the type inference when being applied in the case where _Object-as-a-__Record_ was the intention.
The type {p1: T1, p2: T2, ...pn: Tn}
will be coerced to the {[key: string]: T1 | T2 | ... | Tn }
conflating all the types and effectively discarding all the information about the structure of a record:
console.log(res.a[0].toFixed(2))
will be rejected by the compiler;console.log((<number>res['a'][0]).toFixed(2))
has two uncontrolled and error-prone places: the type-cast and arbitrary property name.1, 2 — for the programmers who migrates from the ES to TS
type Numbers<p extends string> = { ...p: number };
type NumbersOpt<p extends string> = {...p?: number };
type ABC = "a" | "b" | "c";
type abc = Numbers<ABC> // abc = {a: number, b: number, c: number}
type abcOpt = NumbersOpt<ABC> // abcOpt = {a?: number, b?:number, c?: number}
function toFixedAll<p extends string>(obj: {...p: number}, precision):{...p: string} {
var result: {...p: string} = {} as any;
Object.keys(obj).forEach((p:p) => {
result[p] = obj[p].toFixed(precision);
});
return result;
}
var test = toFixedAll({x:5, y:7}, 3); // { x: "5.00", y: "6.00" }, p inferred as "x"|"y"
console.log(test.y.length) // 4 test.y: string
example:
declare function mapValues<p extends string, T1[p], T2[p]>(
obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
example:
class C<Array<T>> {
x: T;
}
var v1: C<string[]>; // v1.x: string
with all three steps together we could write
declare module Backbone {
class Model<{...p: T[p]}> {
get(prop: p): T[p];
set(prop: p, value: T[p]): void;
}
}
declare module ImmutableJS {
class Map<{...p: T[p]}> {
get(prop: p): T[p];
set(prop: p, value: T[p]): this;
}
}
declare function pluck<p extends string, T[p]>(
arr: Array<{...p:T[p]}>, prop: p
): Array<T[p]>
I know that the syntax is more verbose than the one proposed by @fdecampredon
but I can't imagine how to express the type of the mapValues
or the combineReducers
with the original proposal.
function combineReducers(reducers) {
return (state, action) => mapValues(reducers, (reducer, key) => reducer(state[key], action))
}
with my proposal its declaration will look like:
declare function combineReducers<p extends string, S[p]>(
reducers: { ...p: (state: S[p], action: Action) => S[p] }
): (state: { ...p: S[p] }, action: Action) => { ...p: S[p] };
Is it expressible with the original proposal?
@spion, @fdecampredon ?
@Artazor Definitely an improvement. It now seems to be good enough to express my benchmark use case of this feature:
Assuming user.id
and user.name
are database column types containing number
and string
accordingly i.e. Column<number>
and Column<string>
Write the type of a function select
that takes:
select({id: user.id, name: user.name})
and returns Query<{id: number; name: string}>
select<T>({...p: Column<T[p]>}):Query<T>
Not sure hows that going to be checked and inferred though. Seems like there might be a problem, as the type T doesn't exist. Would the return type need to be expressed in a destructured form? i.e.
select<T>({...p: Column<T[p]>}):Query<{...p:T[p]}>
@Artazor forgot to ask, is the p extends string
parameter really necessary?
Wouldn't something like this be enough?
select<T[p]>({...p: Column<T[p]>}):Query<{...p:T[p]}>
i.e. for all type variables T[p] in {...p:Column<T[p]>}
edit: another question, how is this going to be checked for correctness?
@spion I've got your point of view — you've misunderstood me (but it's my fault), by the T[p]
I've meant an entirely different thing, and you will see that p extends string
is a crucial thing here. Let me explain my thoughts in depth.
Let's consider four use cases for data structures: List
, Tuple
, Dictionary
and Record
. We'll refer to them as _conceptual data structures_ and describe them with the following properties:
Conceptual data structures | Access by | ||
---|---|---|---|
a. position (number) | b. key (string) | ||
Count of items | 1. variable (the same role for each item) | List | Dictionary |
2. fixed (each item plays its own unique role) | Tuple | Record |
In JavaScript, these four cases are backed by two storage forms: an Array
— for a1 and a2, and an Object
— for b1 and b2.
_Note 1_. To be honest, it is not correct to say that the
Tuple
is backed by theArray
, as the only "true" tuple type is the type of thearguments
object which is not anArray
. Nevertheless, they are more or less interchangeable, especially in the context of the destructuring and theFunction.prototype.apply
which opens the door for the meta-programming. The TypeScript also leverages Arrays for modeling Tuples._Note 2_. While the full concept of the Dictionary with arbitrary keys was introduced decades later in form of the ES6
Map
, Brendan Eich's initial decision to conflate aRecord
and a limitedDictionary
(with string keys only) under the same hood ofObject
(whereobj.prop
is equivalent to theobj["prop"]
) became the most controversial element of the entire language (in my opinion).
It is both the curse and the blessing of the JavaScript semantics. It makes reflection trivial and encourages programmers freely switch between _programming_ and _meta-programming_ levels at almost zero mental cost (even without noticing!). I believe it is the essential part of the JavaScript success as the scripting language.
Now it is the time for the TypeScript to provide the way for expressing types for this weird semantics. When we think at the _programming_ level all is ok:
Types at the programming level | Access by | ||
---|---|---|---|
a. position (number) | b. key (string) | ||
Count of items | 1. variable (the same role for each item) | T[] | {[key:string]:T} |
2. fixed (each item plays its own unique role) | [T1, T2, ...] | {key1: T1, key2: T2, ...} |
However, when we switch to the _meta-programming_ level, then things that were fixed at the _programming_ level suddenly become variable! And here we suddenly recognize relations between tuples and function signatures and propose things like #5453 (Variadic Kinds) that in fact covers only small (but quite important) part of the metaprogramming needs — the concatenation of signatures by giving the ability to destructure a formal parameter into types of the rest-args:
function f<T>(a: number, ...args:T) { ... }
f<[string,boolean]>(1, "A", true);
In case, if #6018 also will be implemented, the Function class could look like
declare class Function<This, TArgs, TRes> {
This::(...args: TArgs): TRes;
call(self: This, ...args: TArgs): TRes;
apply(self: This, args: TArgs): TRes;
// bind needs also formal pattern matching:
bind<[...TPartial, ...TCurried] = TArgs>(
self: This, ...args: TPartial): Function<{}, TCurried, TRes>
}
It is great but anyway incomplete.
For example, imagine that we want to assign a correct type to the following function:
function extractAndWrapAll(...args) {
return args.map(x => [x]);
}
// wrapAll(1,"A",true) === [[1],["A"],[true]]
With proposed variadic kinds we can't transform types of the signature. Here we need something more powerful. In fact, it is desirable to have compile-time functions that can operate over types (as in Facebook's Flow). Though, I'm sure this will be possible only when (and if) the type system of the TypeScript would be stable enough to expose it to the end programmers without significant risk to break a user-land on a next minor update. Thus, we need something less radical than the full meta-programming support but still capable of dealing with demonstrated problems.
To address this problem, I want to introduce the notion of _object signature_. Roughly it is either
Ideally, it would be nice to have an integer literal types as well as string literal types to represent signatures of the arrays or tuples like
type ZeroToFive = 0 | 1 | 2 | 3 | 4 | 5;
// or
type ZeroToFive = 0 .. 5; // ZeroToFive extends number (!)
the rules for integer literals are congruent to the string literal ones;
Then we can introduce signature abstraction syntax, that exactly matches to the object rest/spread syntax:
{...Props: T }
with one significant exception: here Props
is the identifier that is bound under the universal quantifier:
<Props extends string> {...Props: T } // every property has type T
<Index extends number> {...Index: T } // every item has type T
// the same as T[]
thus, it is introduced in type lexical scope and can be used anywhere in this scope as the name of the type. However, its usage is dual: when used in place of rest/spread property name it represents abstraction over the object signature when it is used a standalone type it represents a subtype of the string (or number respectively).
declare class Object {
static keys<p extends string, q extends p>(object{...q: {}}): p[];
}
And here is the most sophisticated part: _Key Dependent Types_
we introduce special type construct: T for Prop
(I'd like to use this syntax rather than T[Prop] that confused you) where Prop is the name of the type variable that holds object's signature abstraction. For example <Prop extends string, T for Prop>
introduces two formal types into type lexical scope, the Prop
and the T
where it is known that for each particular value p
of Prop
there will be it's own type T
.
We do not say that somewhere is an object that has properties
Props
and their types areT
! We only introduce a functional dependency between two types. Type T is correlated with members of type Props, and that's all!
It gives us possibility to write such things as
function unwrap<P extends string, T for P>(obj:{...P: Maybe<T>}): Maybe<{...P: T}> {
...
}
unwrap({a:{value:1}, b:{value:"A"}, c:{value: true}}) === { a: 1, b: "A", c: true }
// here actual parameters will be inferred as
unwrap<"a"|"b"|"c", {a: number, b: string, c: boolean}>
however the second parameter is treated not as an object but rather as the abstract map of identifiers to types. In this perspective T for P
can be used to represent abstract sequences of types for tuples when P is subtype of number (@JsonFreeman ?)
When T
is used somewhere inside {...P: .... T .... }
it represents exactly one particular type of that map.
It is my main idea.
Eagerly awaiting any questions, thoughts, criticism -)
Right, so extends string
is to take arrays (and variadic arguments) into account in one case, and string constants (as types) in the other. Sweet!
We do not say that somewhere is an object that has properties Props and their types are T ! We only introduce a functional dependency between two types. Type T is correlated with members of type Props, and that's all!
I didn't mean that, I meant it more as their types are T[p], a dictionary of types indexed by p. If thats a good intuition, I'd keep that.
Overall though, the syntax might need a bit more work, but the general idea looks awesome.
Is it possible to write unwrap
for variadic arguments?
edit: nevermind, I just realised that your proposed extension to variadic kinds addresses this.
Hello everybody,
I am puzzling how to solve one of my problem and found this discussion.
Problem is:
I have RpcManager.call(command:Command):Promise<T>
method, and usage would be so:
RpcManager.call(new GetBalance(123)).then((result) => {
// here I want that result would have a type.
});
Solution I think could be like:
interface Command<T> {
responseType:T;
}
class GetBalance implements Command<number> {
responseType: number; // somehow this should be avoided. maybe Command should be abstract class.
constructor(userId:number) {}
}
class RpcManager {
static call(command:Command):Promise<typeof command.responseType> {
}
}
or:
class RpcManager {
static call<T>(command:Command<T>):Promise<T> {
}
}
Any thoughts on this?
@antanas-arvasevicius the last code block in that example should do what you want.
it sounds like you have more of a question of how to accomplish a specific task; please use Stack Overflow or file an issue if you think you have found a compiler bug.
Hi, Ryan, thank you for your response.
I've tried that last block of code but it doesn't work.
Quick demo:
interface Command<T> { }
class MyCommand implements Command<{status:string}> { }
class RPC { static call<T>(command:Command<T>):T { return; } }
let response = RPC.call(new MyCommand());
console.log(response.status);
//output: error TS2339: Property 'status' does not exist on type '{}'.
//tested with: Version 1.9.0-dev.20160222
Sorry that I didn't use Stack Overflow, but I thought that it was related to this issue :)
Should I open an new issue about this topic?
An unconsumed generic type parameter prevents inference from working; in general you should _never_ have unused type parameters in type declarations as they are meaningless. If you consume T
, everything just works:
interface Command<T> { foo: T }
class MyCommand implements Command<{status:string}> { foo: { status: string; } }
class RPC { static call<T>(command:Command<T>):T { return; } }
let response = RPC.call(new MyCommand());
console.log(response.status);
That is just amazing! Thank you!
I haven't thought that type parameter can be placed inside generic type and
TS will extract it.
On Feb 22, 2016 11:56 PM, "Ryan Cavanaugh" [email protected] wrote:
An unconsumed generic type parameter prevents inference from working; in
general you should _never_ have unused type parameters in type
declarations as they are meaningless. If you consume T, everything just
works:interface Command
{ foo: T }class MyCommand implements Command<{status:string}> { foo: { status: string; } }class RPC { static call (command:Command ):T { return; } }
let response = RPC.call(new MyCommand());
console.log(response.status);—
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment-187404245
.
@antanas-arvasevicius if you are creating RPC style APIs I have some docs you might find useful : https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md :rose:
Approaches above seem:
string
are not "Find all references"-able, nor refactorable (e.g. renames).Here's another idea, inspired by C# Expression trees. This is just a rough idea, nothing fully thought-out! Syntax is terrible. I just want to see if this inspires someone.
Assume we have a special kind of strings to denote expressions.
Let's call it type Expr<T, U> = string
.
Where T
is the starting object type and U
is the result type.
Assume we could create an instance of Expr<T,U>
by using a lambda that takes one parameter of type T
and performs a member access on it.
For example: person => person.address.city
.
When this happens, the whole lambda is compiled to a string containing whatever access on the parameter was, in this case: "address.city"
.
You can use a plain string instead, which would be seen as Expr<any, any>
.
Having this special Expr
type in the language enables stuff like that:
function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];
let numbers = pluck([{x: 1}, {x: 2}], p => p.x); // number[]
// compiles to:
// let numbers = pluck([..], "x");
This is basically a limited form of what Expressions are used in C# for.
Do you think this could be refined and lead somewhere interesting?
@fdecampredon @RyanCavanaugh
_(@jods4 - I'm sorry I'm not replying to your suggestion here. I hope it doesn't get 'buried' through the comments)_
I think the naming of this feature ('Type Property type') is highly confusing and very difficult to understand. It was _very_ hard to even figure out what the concept described here was and what it means in the first place!
First, not all types have properties! undefined
and null
don't (though these are only recent additions to the type system). Primitives like number
, string
, boolean
are rarely indexed by a property (e.g. 2["prop"]? though this does seem to work, it almost always a mistake)
I would suggest naming this issue Interface property type reference through string literal values. The topic here is not about introducing a new 'type', but a very particular way to reference an existing one using a string variable or function parameter who's value _must be known at compile time_.
It would have been highly beneficial if this was described and exemplified as simply as possible, outside of the context of particular use case:
interface MyInterface {
prop1: number;
prop2: string;
}
let prop1Name = "prop1";
type Prop1Type = MyInterface[prop1Name]; // Prop1Type is now 'number'
let prop2Name = "prop2";
type Prop2Type = MyInterface[prop2Name]; // Prop2Type is now 'string'
let prop3Name = "prop3";
type NonExistingPropType = MyInterface[prop3Name]; // Compilation error: property 'prop3' does not exist on 'MyInterface'.
let randomString = createRandomString();
type NotAvailablePropType = MyInterface[randomString]; // Compilation error: value of 'randomString' is not known at compile time.
_Edit: It seems like in order to implement this correctly the compiler must know for sure that the string assigned to the variable has not changed between the point it was initialized and the point it was referenced in the type expression. I don't think this is very easy? Would this assumption about the run-time behavior always hold?_
_Edit 2: Perhaps this would only work with const
when used with a variable?_
I'm not sure if the original intention was to only allow the property reference at the very particular case of a string literal is passed to a function or method parameter? e.g.
function func(someString: string): MyInterface[someString] {
..
}
let x = func("prop"); // x gets the type of MyInterface.prop
Have I generalized this beyond the what originally intended?
How would this handle a case where the function argument is not a string literal, i.e. not known at compile time? e.g.
let x = func(getRandomString()); // What type if inferred for 'x' here?
Will it error, or default to any
perhaps?
(P.S: If this was indeed the intention, then I will suggest renaming this issue to Interface property type reference by string literal function or method arguments - As lengthy as this turned out, this is much more accurate and explanatory than the current title.)
Here is a simple benchmark example (enough for an illustration) that shows what this feature needs to enable:
Write the type of this function, which:
function awaitObject(obj) {
var result = {};
var wait = Object.keys(obj)
.map(key => obj[key].then(val => result[key] = val));
return Promise.all(wait).then(_ => result)
}
When called on the following object:
var res = awaitObject({a: Promise.resolve(5), b: Promise.resolve("5")})
The result res
should be of type {a: number; b: string}
With this feature (as fully fleshed out by @Artazor) the signature would be
awaitObject<p, T[p]>(obj: {...p: Promise<T[p]>}):Promise<{...p: T[p]}>
edit: fixed the return type above, was missing Promise<...>
^^
@spion
Thanks for trying to provide an example, but nowhere here I've found a reasonable and concise specification and a set of clear reduced examples that are _detached_ from practical applications and try to highlight the semantics and the various edge cases of how this would work, even something as basic as:
function func<T extends object>(name: string): T[name] {
...
}
name
is not known at compile time, null, or undefined, e.g. func<MyType>(getRandomString())
, func<MyType>(undefined)
.T
is a primitive type like number
or null
.T[name]
can be used in the function body. And in that case, what happens if name
is reassigned in the function body and thus its value may no longer known at compile time?T["propName"]
or T[propName]
work as well by its own? without a reference to a parameter or variable, I mean - it could be useful!T[name]
can be used in other parameters or even outside of function scopes, e.g.function func<T extends object>(name: string, val: T[name]) {
...
}
type A = { abcd: number };
const name = "abcd";
let x: A[name]; // Type of 'x' resolves to 'number'
_7. No real discussion of the relatively simple workaround using an additional generic parameter and typeof
(though it was mentioned, but relatively late after the feature had already been accepted):
function get<T, V>(obj: T, propName: string): V {
return obj[propName];
}
type MyType = { abcd: number };
let x: MyType = { abcd: 12 };
let result = get<MyType, typeof x.abcd>(x, "abcd"); // Type of 'result' is 'number'
In conclusion: There is no real proposal here, only a set of use case demonstrations. I'm surprised this has been accepted or even received positive interest by the TypeScript team at all because I wouldn't otherwise believe this would be up to their standards. I'm sorry if I may sound a bit harsh here (not typical of me) but none of my criticism is personal or directed towards any particular individual.
Do we really want to go that far out there in terms of meta-programming?
JS is a dynamic language, code can manipulate objects in arbitrary ways.
There are limits to what we can model in the type system and it's easy to come up with functions that you can't model in TS.
The question is rather: how far is it reasonable and useful to go?
Last example (awaitObject
) from @spion is at the same time:
BTW @spion you didn't get the return type of your example right... It returns a Promise
.
We are far from the original issue, which was about typing API that take a string representing fields, such as _.pluck
Those APIs _should be_ supported because they are commonplace and not that complicated. We don't need meta-models such as { ...p: T[p] }
for that.
There are several examples in the OP, some more from Aurelia in my comment in the nameof issue.
A different approach can cover those use-cases, see my comment above for a possible idea.
@jods4 its not narrow at all. It can be used for a number of things that are impossible to model in the type system at the moment. I'd say its one of the last large missing parts in the TS type system. There are plenty of real world examples above by @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment-177287714 the simpler stuff above, the sql query builder "select" example and so on.
This is awaitObject
in bluebird. Its a real method. The type is currently useless.
A more general solution will be better. It will work for both the simple and the complex cases. An underpowered solution will be found lacking for half of the cases and will easily cause compatibility problems if its necessary to adopt a more general one in the future.
Yes it needs a lot more work, but I also think @Artazor did an excellent job analyzing and exploring all the aspects that we haven't thought of before. I wouldn't stay we've strayed from the original problem, we only have a better understanding of it.
We may need a better name for it, I'd try but I don't usually get these things right. How about "Object generics"?
@spion, just for correctnes:
awaitObject<p,T[p]>(obj: {...p:Promise<T[p]>}): Promise<{...p:T[p]}>
(you've missed a Promise in output signature)
-)
@jods4, I agree with @spion
There are plenty of real-world problems where { ...p: T[p] }
is the solution. To name one: react+flux/redux
@spion @Artazor
I'm just worried that this is becoming quite complex to specify precisely.
The motivation behind this issue has drifted. Originally it was about supporting APIs that accept a string to denote an object field. Now it's mostly discussing APIs that use objects as maps or records in a very dynamic fashion. Note that if we can kill two birds with one stone I'm all for it.
If we go back to the APIs that take strings problem, the T[p]
isn't a complete solution in my opinion (I said why before).
_Just for correctness_ awaitObject
should also accept non-Promise properties, at least Bluebird props
does. So now we have:
awaitObject<p,T[p]>(obj: { ...p: T[p] | Promise<T[p]> }): Promise<T>
I changed the return type to Promise<T>
because I expect this notation to work.
There are other overloads, e.g. one that takes a Promise for such an object (its signature is even more fun). So that means the { ...p }
notation needs to be considered a worse match than any other type.
Specifying all of this is going to be hard work. I would say it's the next step if you want to push this forward.
@spion @jods4
I wanted to mention that this feature is not about generics and not about higher kind polymorphic types. This is simply a syntax to reference the type of a property through a containing type (with some "advanced" extensions that are described below), which is not that far from typeof
in concept. Example:
type MyType = { abcd: number };
let y: MyType["abcd"]; // Technically this could also be written as MyType.abcd
Now compare to:
type MyType = { abcd: number };
let x: MyType;
let y: typeof x.abcd;
There are two main differences with typeof
. Unlike typeof
, this applies to types (at least non-primitive ones, a fact that is often omitted here) rather than to instances. In addition (and this is a major thing) it was extended to support using literal string constants (which must be known at compile time) as property path descriptors, and generics:
const propName = "abcd";
let y: MyType[propName];
// Or with a generic parameter:
let y: T[propName];
However technically typeof
could have been extended to support string literals as well (this includes the case where x
has a generic type):
let x: MyType;
const propName = "abcd";
let y: typeof x[propName];
And with this extension it could also be used to solve some of the target cases here:
function get<T>(propName: string, obj: T): typeof obj[propName]
typeof
however is more powerful as it supports indefinite amount of nesting levels:
let y: typeof x.prop.childProp.deeperChildProp
And this one only goes one level. I.e. not planned (as far as I'm aware) to support:
let y: MyType["prop"]["childProp"]["deeperChildProp"];
// Or alternatively
let y: MyType["prop.childProp.deeperChildProp"];
I think the scope of this proposal (if this can even be called a proposal at its level of vagueness) is too narrow. It may help solve a particular problem (though there may be alternative solutions), which seems to make many people very eager to promote it. However it is also consuming valuable syntax from the language. Without a broader plan and design-oriented approach it seems unwise to hastily introduce something that as-of-this-date doesn't even have a clear specification.
_Edits: corrected some obvious mistakes in the code examples_
I've researched a bit on the typeof
alternative:
Future language support for typeof x["abcd"]
and typeof x[42]
has been approved, and now falls under #6606, which is currently under development (there is a working implementation).
This goes half the way. With these in place, the rest can be done at several stages:
(1) Add support for string literal constants (or even numeric constants - might be useful for tuples?) as property specifiers in type expressions, e.g.:
let x: MyType;
const propName = "abcd";
let y: typeof x[propName];
(2) Allow applying these specifiers to generic types
let x: T; // Where T should extend 'object'
const propName = "abcd";
let y: typeof x[propName];
(3) Allow these specifiers to be passed, and in practice "instantiate" the references, through function arguments (typeof list[0]
is planned so I believe this could cover more complex cases like pluck
:
function get<T extends object>(obj: T, propertyName: string): typeof obj[propertyName];
function pluck<T extends object>(list: Array<T>, propertyName: string): Array<typeof list[0][propertyName]>;
(object
type here is the one proposed at #1809)
Although more powerful (e.g. may support typeof obj[propName][nestedPropName]
) it is possible this alternative approach may not cover all the cases described here. Please let me know if there are examples that wouldn't seem to be handled by this (one scenario that comes to mind is when the object instance is not passed at all, however, it is hard for me at this point imagine a case where that would be needed, though I guess that is possible).
_Edits: corrected some mistakes in the code_
@malibuzios
Some thoughts:
nameof
or Expression
at the call site..d.ts
definitions, but it generally won't be statically inferrable or even verifiable in TS functions/implementations.The more I think about it, the more Expression<T, U>
looks like the solution for those kind of APIs. It fixes the call site problems, the typing of the result and can be inferrable + verifiable in an implementation.
@jods4
You mentioned in a previous comment way above that pluck
could be implemented with expressions. Although I did use to be very familiar with C#, I never really experimented with them, so I admit I don't really have a very good understanding of them at this point.
Anyway, just wanted to mention TypeScript supports _type argument inference_, so instead of using pluck
, one can just use map
and achieve the same result, which wouldn't even require specifying a generic parameter (it is inferred) and would also give full type checking and completion:
let x = [{name: "John", age: 34}, {name: "Mary", age: 53}];
let result = x.map(obj => obj.name);
// 'result' is ["John", "Mary"] and its type inferred as 'string[]'
Where map
(highly simplified for demonstration) is declared as
map<U>(mapFunc: (value: T) => U): U[];
This same inference pattern can be used as a 'trick' to pass any result of an arbitrary lambda (which doesn't even need to be called) to a function and set its return type to it:
function thisIsATrick<T, U>(obj: T, onlyPassedToInferTheReturnType: () => U): U {
return;
}
let x = {name: "John", age: 34};
let result = thisIsATrick(x, () => x.age) // Result inferred as 'number'
_Edit: it might look silly to pass it in a lambda here and not just the thing itself! however in more complex cases like nested objects (e.g. x.prop.Childprop
) there could be undefined or null references that may error. Having it in a lambda, which is not necessarily called, avoids that._
I admit I'm not very familiar with some of the use cases discussed here. Personally I never felt a need to call a function that takes a property name as string. Passing a lambda (where the advantages you describe also hold) in combination with type argument interface is usually enough to solve many common problems.
The reason I suggested the typeof
alternative approach was mostly because that seems to cover about the same use cases and provide the same functionality for what people describe here. Would I use it myself? I don't know, never felt a real need to (it could be simply that I almost never use external libraries like underscore, I almost always develop utility functions by myself, on a needed basis).
@malibuzios True, pluck
is a bit silly with lambdas + map
now built-in.
In this comment I give 5 different APIs that all take strings -- they are all connected to observing changes.
@jods4 and others
Just wanted to mention that when #6606 is complete, by only implementing stage 1 (allow constant string literals to be used as property specifiers) some of the functionality needed here can be achieved, though not as elegantly as it could (it may require the property name to be declared as a constant, adding an additional statement).
function observeProperty<T, U>(obj: T, propName: string ): Subscriber<U> {
....
}
let x = { name: "John", age: 42 };
const propName = "age";
observeProperty<typeof x, typeof x[propName]>(x, propName);
However, the amount of effort to implement this may be significantly lower than implementing stage 2 and 3 as well (I'm not sure but it's possible that 1 is already covered by #6606). With stage 2 implemented, this would also cover the case where x
has a generic type (but I'm not sure if that's really needed though?).
Edit: The reason I used an external constant and not just wrote the property name twice was not only to reduce typing, but also to ensure the the names always match, though this still cannot be use with tools like "rename" and "find all references", which I find to be a serious disadvantage.
@jods4 I'm still looking for a better solution that doesn't make use of strings. I'll try to think what can be done with nameof
and its variants.
Here's another idea.
Since string literals are already supported as types, e.g. one can already write:
let x: "HELLO";
One can view a literal string passed to a function as a form of generic specialization
(_Edit: these examples was corrected to ensure that s
is immutable in the function body, hough const
isn't supported at parameter positions at this point (not sure about readonly
though)._) :
function func(const s: string)
The string
parameter type associated with s
can be viewed as an implicit generic here (as it can be specialized to string literals). For clarity I will write it more explicitly (though I'm not sure if there's really a need to):
function func<S extends string>(const s: S)
func("TEST");
could internally specialize to:
function func(s: "TEST")
And now this can be used as an alternative way to "pass" the property name, which I feel captures the semantics better here.
function observeProperty<T, S extends string>(obj: T, const propName: S): Subscriber<T[S]>
x = { name: "John", age: 33};
observeProperty(x, nameof(x.age))
Since in T[S]
, both T
, and S
are generic parameters. It seems more natural to have two types combined in a type position than mixing run-time and type scope elements (e.g. T[someString]
).
_Edits: rewritten examples to have an immutable string parameter type._
Since I'm using TS 1.8.7
and not the latest version, I wasn't aware that in more recent versions in
const x = "Hello";
The type of x
is already inferred as the literal type "Hello"
, (i.e.: x: "Hello"
) which makes a lot of sense (see #6554).
So naturally if there was a way to define a parameter as const
(or maybe readonly
would also work?):
function func<S extends string>(const s: S): S
Then I believe this could hold:
let result = func("abcd"); // type of 'result' inferred as the literal type "abcd"
Since some of this is rather new and relies on recent language features, I'll try to summarize it as clearly as I can:
(1) When a const
(and maybe readonly
as well?) string variable receives a value that is known at compile-time, the variable automatically receives a literal type that has the same value (I believe this is a recent behavior that doesn't happen on 1.8.x
), e.g.
const x = "ABCD";
Type of x
is inferred to be "ABCD"
, not string
!, e.g. one may state this as x: "ABCD"
.
(2) If readonly
function parameters were allowed, a readonly
parameter with a generic type would naturally specialize its type to a string literal when it is received as an argument, as the parameter is immutable!
function func<S extends string>(readonly str: S);
func("ABCD");
Here S
has been resolved to the type "ABCD"
, not string
!
However, if the str
parameter was not immutable, the compiler cannot guarantee that it is not reassigned in the body of the function, thus, the type inferred would be just string
.
function func<S extends string>(str: S) {
str = "DCBA"; // This may happen
}
func("ABCD");
(3) It is possible to take advantage of this and modify the original proposal to have the property specifier be a reference to a type, which would need to be constrained to be a derivative of string
(in some cases it may be even appropriate to constrain it to singleton literal types only, though there isn't currently really a way to do that), rather than a run-time entity:
function get<T extends object, S extends string>(obj: T, readonly propName: S): T[S]
Calling this would not require explicitly specifying any type arguments, as TypeScript supports type argument inference:
let x = { name: "John", age: 42 };
get(x, "age"); // result type is inferred to be 'number'
// or for stronger type safety:
get(x, nameof(x.age)); // result type is inferred to be 'number'
_Edits: corrected some spelling and code mistakes._
_Note: a generalized and extended version of this modified approach is now also tracked in #7730._
Here is another use of type property type (or "indexed generics" as I like to call them lately) that came up in a discussion with @Raynos
We can already write the following general checker for arrays:
function tArray<T>(f:(t:any) => t is T) {
return function (a:any): a is Array<T> {
if (!Array.isArray(a)) return false;
for (var k = 0; k < a.length; ++k)
if (!f(a[k])) return false;
return true;
}
}
function tNumber(n:any): n is number {
return typeof n === 'number'
}
var isArrayOfNumber = tArray(tNumber)
function test(x: {}) {
if (isArrayOfNumber(x)) {
return x[x.length - 1].toFixed(2); // this type checks
}
}
Once we have indexed generics, we would also be able to write a general checker for objects:
function tObject<p, T[p]>(checker: {...p: (t:any) => t is T[p]}) {
return function(obj: any): obj is {...p: T[p] } {
for (var key in checker) if (!checker[key](obj[key])) return false;
return true;
}
}
which together with the primitive checkers for string, number, boolean, null and undefined would let you write things like this:
var isTodoList = tObject({
items: tArray(tObject({text: tString, completed: tBoolean})),
showCompleted: tBoolean
})
and have it result with the right runtime checker _and_ compile time type guard, at the same time :)
Has anybody done any work on this yet, or is it on anybody's radar? This will be a major improvement to how many standard libraries can use typings, including the likes of lodash
or ramda
and many database interfaces.
@malibuzios I believe you are approaching @Artazor 's suggestion :)
To address your concerns:
function func<S extends string>(readonly str: S): T[str] {
...
}
This would be
function func<S extends string, T[S]>(str: S):T[S] { }
This way, the name would be locked to the most specific type (a string constant type) when called with:
func("test")
The type S
becomes "test"
(not string
). As such str
cannot be reassigned a different value. If you tried that, e.g.
str = "other"
The compiler could produce an error (unless there are variance soundness issues ;))
Instead of just getting the type of one property I would like to have the option to get an arbitrary super type of type.
So my suggestion is to add the following syntax: Instead of just having T[prop]
, I would like to add the syntax T[...props]
. Where props
must be an array of members of T
. And the resulting type is a super type of T
with members of T
defined in props
.
I think it would be highly useful in Sequelize — a popular ORM for node.js. For security reasons and for perf reasons, it is wise to just query attributes in a table that you need to use. That often means super type of a type.
interface IUser {
id: string;
name: string;
email: string;
createdAt: string;
updatedAt: string;
password: string;
// ...
}
interface Options<T> {
attributes: (memberof T)[];
}
interface Model<IInstance> {
findOne(options: Options<IInstance>): IInstance[...options.attributes];
}
declare namespace DbContext {
define<T>(): Model<T>;
}
const Users = DbContext.define<IUser>({
id: { type: DbContext.STRING(50), allowNull: false },
// ...
});
const user = Users.findOne({
attributes: ['id', 'email', 'name'],
where: {
id: 1,
}
});
user.id
user.email
user.name
user.password // error
user.createdAt // error
user.updatedAt // error
(In my example it includes the operator memberof
, which is what you expect it to be, and also the expression options.attributes
which is the same as typeof options.attributes
, but I think the typeof
operator is redundant in this case, because it is in a position that expects a type.).
If no one insist, I started working on this.
What's your thought about type safety inside the function, i.e. ensure a return statement returns something assignable to a return type?
interface A {
a: string;
}
function f(p: string): A[p] {
return 'aaa'; // This is string, but can we ensure it is the intended A[p] ?
}
Also the name "Property Type" used here seems a bit incorrect. It sort of states that the type has properties, which nearly all types have.
What about "Property Referenced Type"?
What's your thought about type safety inside the function
My brainstorming:
let a: A;
function f(p: string): A[p] {
let x = a[p]; // typeof A[p], only when:
// 1. p is directly referencing function argument
// 2. function return type is Property Reference Type
p = "abc"; // not allowed to assign a new value when p is used on Property Reference Type
return x; // x is A[p], so okay
}
And disallow normal string on the return line.
@malibuzios problem with your idea is that it requires specifying all the generics in case they're not possible to infer, which can be viral / polluting the code-base.
Any comments from the TS team about https://github.com/Microsoft/TypeScript/issues/1295#issuecomment-239653337?
@RyanCavanaugh @mhegazy etc?
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:
P
can only be a string literal?P extends string
wouldn't be a very ideomaticP super string
constraint (#7265, #6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)T
has an indexer of string
s or number
s. then P
can be string
or number
."something"
as a second argument, it will be of type string
P
should be used if it's acceptable{ [i: string]: number /* | undefined */ }
undefined
in the domain?P
, T
and R
is a key to make it work@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:
keysof Foo
operator to grab the union of property names from Foo
as string literal types.Foo[K]
type which specifies that for some type K
that is a string literal types or union of string literal types.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, K
will be a subtype of keysof T
which means that it is a string literal type or a union of string literal types. Anything you pass in for the key
parameter should be contextually typed by that literal/union of literals, and inferred as a singleton string literal type.
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 keysof
often needs to "delay" its operation. It is too eager in how it evaluates a type, which is a problem for type parameters like in the first example I posted (i.e. the case we really want to solve is actually the hard part :smile:).
Hope that gives you all an update.
@DanielRosenwasser Thanks for the update. I just saw @weswigham submitted a PR about the keysof
operator, so it is maybe better to hand this issue off to you guys.
I just wonder why you decided to depart from the original proposed syntax?
function get(prop: string): T[prop];
and introduce keysof
?
T[prop]
is less general, and requires a lot of interlaced machinery. One big question here is how you'd even relate the literal contents of prop
to property names of T
. I'm not even completely sure what you'd do. Would you add an implicit type parameter? Would you need to change contextual typing behavior? Would you need to add something special to signatures?
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 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]
.
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 ?
@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 _.pick
:
o2 = _.pick(o1, 'p1', 'p2');
pick(Object, ...props: String[]) : WHAT GOES HERE;
@rtm I suggested it in https://github.com/Microsoft/TypeScript/issues/1295#issuecomment-234724380. Though it might be better to open a new issue, even though it is related to this one.
Implementation now available in #11929.
Most helpful comment
@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:
keysof Foo
operator to grab the union of property names fromFoo
as string literal types.Foo[K]
type which specifies that for some typeK
that is a string literal types or union of string literal types.From these basic blocks, if you need to infer a string literal as the appropriate type, you can write
Here,
K
will be a subtype ofkeysof T
which means that it is a string literal type or a union of string literal types. Anything you pass in for thekey
parameter should be contextually typed by that literal/union of literals, and inferred as a singleton string literal type.For instance
Biggest issue is that
keysof
often needs to "delay" its operation. It is too eager in how it evaluates a type, which is a problem for type parameters like in the first example I posted (i.e. the case we really want to solve is actually the hard part :smile:).Hope that gives you all an update.