-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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"]>;