0

I'm trying to return value from one of model levels. First level works perfectly because type of returning value is T[K] which means ['Login', 'Password', 'Address'] return what i want. But when i change getProperty(auth, x => x.Address); to getProperty(auth, x => x.Address.Address2);. I know the 'Address2' property is not directly under the T type. And i have no idea for what type change 'T[K]' to works also with ie. Address2 property. Could you help me?

Edit: what's weird getProperty(auth, x => x.Address.Address2.State); works

export interface Auth {
  Login: string;
  Password: string;
  Address: {
      Address2: {
        State: string;
      }
  }
}

let auth: Auth = {
    Login: 'login',
    Password: 'password',
    Address: {
        Address2:{
            State: 'some state'
        }
    }
  };


function getProperty<T, K extends keyof T>(obj: T, fn: (m: T) => T[K]) {
    console.log(fn(obj));
}

getProperty(auth, x => x.Address.Address2);
Dakito
  • 347
  • 1
  • 4
  • 9
  • Note: `x.Address.Address2.State` only works because it is a string and `fn` expects a string returned ( since `keyof T` is a string ) – Patrick Hollweck Feb 17 '19 at 20:51
  • im confused `getProperty(auth, x => x.Address.Address2)` is just the more-verbose version of just `x.Address.Address2` why would you not just avoid the function call and use the latter – Shanon Jackson Feb 17 '19 at 21:31

2 Answers2

1
function getProperty<T, B>(obj: T, fn: (m: T) => B): B {
   return fn(obj);
}

const testType = getProperty(auth, x => x.Address.Address2); // string.

Hope this helps, as far as i'm aware there's no way to type-deep the fact that you're function returns a type resulting from an index of T because there's no way to express this "deep". I.E You can express T[K] but only to a depth of 1. An alternative syntax which can type'fully express deep key access is something like this......

interface IGetProperty<Original, Access> {
    value: Access,
    pick: <K extends keyof Access>(key: K) => IGetProperty<Original, Access[K]>
}

const getProperty = <Original, Access = Original>(obj: Original): IGetProperty<Original, Access> => {
   return {
       value: obj as any,
       pick: (key) => getProperty((obj as any)[key as any]) as any
   }
}

const testType = getProperty(auth).pick("Address").pick("Address2").pick("State").value // string.
const testType = getProperty(auth).pick("Address").pick("Invalid Key").pick("State").value // Error.

EDIT: In typescript even if you could type a function to return T[K] that will not "force" the function to return some value indexed from "T" because if T[K] is string then you could return any string such as "hello" that didn't come from "T" and it would still type-check, this happens because Typescript types things structurally not nominally this behaviour would be possible if you're T[K]'s had types which were globally unique

Shanon Jackson
  • 5,873
  • 1
  • 19
  • 39
  • This fixes the warning, but does not answer the question, since the function could now return anything, and not just a property of the object typed T. For example, this will allow: getProperty(auth, x => 'hello'), which is not right – jo_va Feb 17 '19 at 21:41
  • 1
    Iv'e added an alternative syntax hopefully this helps – Shanon Jackson Feb 17 '19 at 21:44
  • [This question](https://stackoverflow.com/questions/49005179/typescript-infer-type-of-nested-keyof-properties) might help too. – jo_va Feb 17 '19 at 21:53
  • 1
    Theres two problems with syntax like that, the first problem is that you need overloads up to N keys meaning it only supports "finite" depth. The second problem is that it requires the keys to be strings meaning you still wont be able to use the function syntax and have it typecheck – Shanon Jackson Feb 17 '19 at 22:01
0

I'm not 100% sure I understand the use case, but you might be able to restrict the callback to "something that returns a (possibly nested) property" by using a phantom type that doesn't exist at runtime. Something like this:

  declare class DeepDooDoo {private dooDoo: true};
  type DeepVooDoo<T> = DeepDooDoo & (T extends object ? {
    [K in keyof T]: DeepVooDoo<T[K]>
  } : T)

  function getProperty<T, U>(obj: T, fn: (m: DeepVooDoo<T>)=>DeepVooDoo<U> ): void {
    console.log(fn(obj as DeepVooDoo<T>));
  }

The idea is that we pretend that the passed-in object isn't of type T, but of type DeepVooDoo<T> instead. This is an instance of a phantom class DeepDooDoo, and (due to the recursively mapped type definition) all of its properties are themselves DeepDooDoo instances, and all of their properties, etc. The callback function is restricted to something that accepts a DeepVooDoo<T> and returns a DeepVooDoo<U> for some (inferrable) U. This more or less means that you have to return either a property or subproperty (or subsubproperty etc.) of your passed-in object. If you try to return something else the compiler will likely notice that is not a DeepDooDoo and complain.

Let's try:

  getProperty(auth, x => x.Address.Address2); // okay
  getProperty(auth, x => "hello"); // error! string is not assignable to DeepDooDoo

Looks good as far as it goes. Phantom types are a bit of an abuse of the type system, so it's quite possible there could be strange side effects. If you decide to go ahead with something like this, tread carefully and make sure it fits your use case.

Okay, hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Yeah i thought about doing something bypassing typescripts structural typing and going to "nominal" which i made a mention of in the edit, good solution. – Shanon Jackson Feb 17 '19 at 22:17
  • @jcalz It took me all day to understand how you figured out, but now I know, appreciate for you assistance:) – Dakito Feb 18 '19 at 18:24