Skip to content

Feature request: Multiple level deep Static types for dynamically named properties. #12407

Closed
@canufeel

Description

@canufeel

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"]>;

Related:

#11929
#12342

Metadata

Metadata

Labels

Needs More InfoThe issue still hasn't been fully clarified

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions