#Index nested objects with function arguments?

14 messages · Page 1 of 1 (latest)

chilly verge
#

I'm picking up TS again and I can't believe this is so hard to do. I must be missing something obvious

const foo = {a: {b: 1}, u: {v: 2}} as const;

const f = (x: 'a'|'u', y: 'b'|'v') => {
    const foo_u = foo[x];
    if(v in foo_u) {
        const foo_u_v = foo_u[y]
    }
}

I get the error

Property 'b' does not exist on type '{ readonly b: 1; } | { readonly v: 2; }'

I understand that keyof '{ readonly b: 1; } | { readonly v: 2; }' is never, but this code cannot fail at runtime.

What is the least annoying way to deal with that?

gaunt veldtBOT
#
Burrito#6903

Preview:```ts
const foo = {a: {b: 1}, u: {v: 2}} as const
type Foo = typeof foo

const f = <T extends keyof Foo>(
x: T,
y: keyof Foo[T]
) => {
const foo_u = foo[x]
const foo_u_v = foo_u[y]
}

f("a", "b")
f("a", "v")
f("u", "b")
f("u", "v")```

chilly verge
#

Sadly I can't do that. The situation I am really in is that y is a key in a mapped type, so there is no direct connection between foo_u and y that the compiler can understand, which is why I rely on the computed types rather than on their relations

floral drift
#

Best to show your actual situation.

#

But if you have no choice but to resort to runtime checks like y in foo_u, then there's not really a way for TS to know, you just have to cast it.

chilly verge
#

The thing is you can't cast y as keyof foo_u because you would be casting to never, and if you tried to cast foo[x] you would have to specify the return type of foo[x][y] which is what you are trying to access in the first place.

I ended up doing it dynamically and wrapping the return value in an Option

manic bough
#

@chilly verge You say this can't fail at runtime, but f('a', 'v') is a valid way to call this and that fails at runtime.

chilly verge
#

Sorry, my example as well as the title of the thread were really poorl chosen. I simply encounter this problem in functions because it's in this context that I can end up with values being this kind of unions.

Assume the following

const foo = {a: {b: 1}, u: {v: 2}} as const;

const x = 'a' as 'a'|'u'; // can't change that
const y = 'v' as 'b'|'v'; // can't change that

const foo_u = foo[x];

if(y in foo_u) {
    const foo_u_v = foo_u[y]
}
#

Actually I'm not sure what you mean by "fail at runtime" here.

What I mean by can't fail at runtime is that when I do y in foo_u (I would probably use Object.hasOwn but anyway), foo_u can be safely indexed with y.

manic bough
#

@chilly verge Oh, your original example used v in and foo_u[v].

#

Yeah, that won't fail to index the object at runtime, but it also isn't enough to prove to TS that it's a known key.

#

One approach for these sort of weird-indexing-strategies is to 'upcast' the object to a record:

#
const foo_u: Partial<Record<"b" | "v", number>> = foo[x];
const foo_u_v = foo_u[y]
if(foo_u_v !== undefined) {
  //...
}
chilly verge
#

Although it's an option, you need to hardcode number or write a utility type that unionizes all the leaves of foo, and it does not scale well if foo is deeper and/or unbalanced.

I have to agree I don't encounter this often but I wish there was something comparable to optional chaining that I could do. All I want is that the inferred type is correct and that I am forced to check for undefined or whatnot