Description
Background
In frameworks like Ember the following pattern would be very common:
import * as Ember from 'ember';
const { computed, get } = Ember;
interface myComponent {
myProp: Ember.ComputedProperty<number>;
someMethodThatWouldUseMyProp(): any;
}
var myComponent = {
myProp: computed.readOnly<number>('someDep'),
someMethodThatWouldUseMyProp() {
const myPropValue = get<myComponent, 'myProp'>(this, 'myProp');
}
};
Having a declaration file for Ember like the following:
declare namespace Ember {
class ComputedProperty<T> {
get(keyName: string): T;
/* some more props omitted */
}
namespace computed {
interface readOnly {
<T>(dependentKey: string): ComputedProperty<T>;
}
function readOnly<T>(dependentKey: string): ComputedProperty<T>;
}
interface get {
<T, K extends keyof T>(obj: T, keyName: K): T[K];
}
function get<T, K extends keyof T>(obj: T, keyName: K): T[K];
}
export = Ember;
would result in const myPropValue = get<myComponent, 'myProp'>(this, 'myProp');
inferring a type as Ember.ComputedProperty<number>
which is not what we really want as the way get
method works is not just T[K]
but : T[K]
and then if
the inferred type is Ember.ComputedProperty<F>
it would actually return those F
like so:
type F = number;
interface myComponent {
myProp: Ember.ComputedProperty<F>;
someMethodThatWouldUseMyProp(): any;
}
var myComponent = {
myProp: computed.readOnly<number>('someDep'),
someMethodThatWouldUseMyProp() {
const myPropValue: F = get<myComponent, 'myProp'>(this, 'myProp');
}
};
Proposal
The simplest way to achieve proper behaviour at least as it seems would be to allow using nesting with static types for dynamically named properties:
interface get {
<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): T[K][F];
}
function get<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): T[K][F];
and then redefining the Ember.ComputedProperty
declaration in a such way that we would be able to infer type value from it:
class ComputedProperty<T> {
get(keyName: string): T;
_value: T;
/* some more props omitted */
}
we then would be sure that when we have const myPropValue: F = get<myComponent, 'myProp', '_value'>(this, 'myProp');
we would end up with the correct type for myPropValue
. Now however this way of defining and using get
function would throw: TS2344 Type '_value' does not satisfy the constraint 'never'
Another even better option would be able to have our get
method declaration like the following:
interface get {
<T, K extends keyof T>(obj: T, keyName: K): T[K]["_value"];
}
function get<T, K extends keyof T>(obj: T, keyName: K): T[K]["_value"];
This would allow for even more clean code: const myPropValue: F = get<myComponent, 'myProp'>(this, 'myProp');
. Now it throws either TS2339 Property '_value' does not exist on type T[K]
. Such approach would also allow for multiple level deep computed properties that are supported in Ember:
interface get {
<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): T[K][F]["_value"];
}
function get<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): T[K][F]["_value"];
const myPropValue = get<myComponent, 'someOtherNestedObj', 'someProp'>(this, 'someOtherNestedObj.myProp');
Ideally if we have #12342 we wouldn't have to define the _value
in ComputedProperty in the declaration files and use the result of the get
method instead:
class ComputedProperty<T> {
get(keyName: string): T;
/* some more props omitted */
}
interface get {
<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): ReturnType<T[K][F]["get"]>;
}
function get<T, K extends keyof T, F extends keyof T[K]>(obj: T, keyName: K): ReturnType<T[K][F]["get"]>;