1

is there any way to express that a key-property for an object should be limited by related value? Something equivalent to keyof but for the value related to given key?

const objectAction = <T extends object>(obj: T): void => obj;
const onlyForObjects = <T extends object>(obj: T, key: keyof T): void =>
  objectAction(obj[key])

should work

onlyForObjects<{ a: number, b: object }>({ a: 0, b: {} }, 'b');

should not work

onlyForObjects<{ a: number, b: object }>({ a: 0, b: {} }, 'a');
m. maylon
  • 13
  • 2

2 Answers2

0

If I understood your question correctly, then following is what you are after:

type ObjKeys<T> = {
  [P in keyof T]: T[P] extends Record<any, any> ? P : never
}[keyof T]

const objectAction = <T>(obj: T): void => console.log(obj)

const onlyForObjects = <T>(obj: T, key: ObjKeys<T>): void => objectAction(obj[key])

onlyForObjects<{ a: number; b: Record<string, unknown> }>({ a: 0, b: {} }, 'b') // compiles
onlyForObjects<{ a: number; b: Record<string, unknown> }>({ a: 0, b: {} }, 'a') // does not compile

Pritam Kadam
  • 2,388
  • 8
  • 16
0

You can do that with a generic Record constraint for type parameter T:

const onlyForObjects = <T extends Record<K, object>, K extends string>(
    obj: T, key: K): void => { }

onlyForObjects({ a: 0, b: {} }, 'b'); // works
onlyForObjects({ a: 0, b: {} }, 'a'); // error

Here is a live code sample.

bela53
  • 3,040
  • 12
  • 27
  • In this case, error is at first parameter and not at second. Which means, consumer of this function will not able to pass object with top level key and corresponding primitive value – Pritam Kadam Sep 23 '20 at 10:03
  • If I do this `onlyForObjects({ a: 0, b: {}, c: 'foo' }, 'x')`, error I get is this (which is not what I think expected): error TS2322: Type 'number' is not assignable to type 'object'. error TS2322: Type 'string' is not assignable to type 'object'. – Pritam Kadam Sep 23 '20 at 10:11
  • I don't quite understand what you mean. Look at the first sample line of the playground: `onlyForObjects({ a: 0, b: {}, c: "foo" }, 'b');`. Property `a` has a primitive value type - it works as intended. – bela53 Sep 23 '20 at 10:11
  • You get error (when passed invalid second parameter), thats fine. But the error message which you get is misleading because of the implementation. – Pritam Kadam Sep 23 '20 at 10:13
  • That is also working as intended. We shouldn't be able to select keys, that don't exist on the given object type (like `x`). – bela53 Sep 23 '20 at 10:13
  • I know, it fails to compile but In my opinion, not the right way to do it. With your impl - for this `onlyForObjects({ a: 0, b: {} }, 'a') `, error msg you get is `Type 'number' is not assignable to type 'object'` this on first parameter With my impl, error message you get is : `Argument of type '"a"' is not assignable to parameter of type '"b"'` this on second paramter – Pritam Kadam Sep 23 '20 at 10:18
  • There are certainly a couple of ways to model this type. I see your concern, though could argue, your type might add abstraction cost and verbosity to a single function scoped problem. From my experience, it is a good idea to keep types as simple as possible, while still being complete/wholistic in the type checks. I think, this is just a trade-off between abstraction cost and DX and depends on the requirements at hand. – bela53 Sep 23 '20 at 10:33
  • This seems more like a hack. Here `onlyForObjects({ a: 0, b: {} }, 'a') // error`, first parameter is completely valid, but because you have added type constraint saying that every key should have value of type object. I wont even read 2nd paramter in this case and try to figure out whats wrong with first. But the reality is, 2nd parameter is wrong and not the first. – Pritam Kadam Sep 23 '20 at 10:38
  • hmm almost philosophical: do you have the wrong key for the right object or vice versa the wrong object for the right key? (-: Though you are right, it might be a bit more comprehensible, if the key would be listed first here. – bela53 Sep 23 '20 at 10:57
  • If author says `{ a: 0, b: {} }` this is invalid object (irrespective of what second argument is) and should not be allowed to pass to `onlyForObjects`, then you are perfectly correct ;) – Pritam Kadam Sep 23 '20 at 10:59
  • It is only invalid in `onlyForObjects`, as long as the key does not fit. I think, we won't fully agree on this point ;-). Anyway, thanks for the refreshing discussion (no sarcasm). – bela53 Sep 23 '20 at 11:08
  • PS: changed the constraint from `K extends keyof T` to `K extends string` in the case of unknown keys, which will emit a bit more clear error message as shown [here](https://www.typescriptlang.org/play?#code/MYewdgzgLgBOA2BPAYiATgeQEYCsCmwUEMAvDADwAqMeAHlHmACbEBKB6T5A0gDRy4CUAHz9uNeoxYxoaAJZgA5sIAUAKBiaBOAFwxK-ANZ5Ee7gEo9ANxBympYTADeMAL5qEKdNnyEIKlwBDPQAGfiw9J1c3fgAiWljzAG41IA). – bela53 Sep 23 '20 at 11:13