#Not managing to type properly a react component with one property dependent on another

31 messages · Page 1 of 1 (latest)

torn zephyr
#

I have a dictionary of shape:

const map = {
  SCOPE:{
    ACTION:{
      "default": string,
      "mac"?: string
    }
  }
}

Where "SCOPE" is a finite amount of strings and "ACTION" another finite amount of strings, different for each scope.

And a React component of signature:

export function GodotShortcut({ scope = "general", type }){
}

It's important that the default scope is "general", because it will be by far the most used. The only mandatory property is therefore type, which should be restricted base on the scope.

I am managing to type an interface that does what I want:

GodotShortcut({type:"close scene"}) // works, "close scene" exists in the "general" scope
GodotShortcut({type:"continue"}) // error, "general" doesn't have a "continue"
GodotShortcut({scope:"debugger", type:"continue" }) // works, "continue" exists in the "debugger" scope
GodotShortcut({scope:"debugger", type:"select" }) // error, "debugger" doesn't have a "select"

My issue is that I am not managing to get the proper type inside the function. Of course, I could just shrug and use any, but I would like to understand what I'm doing wrong here. Ideally, it'd all type correctly without type coercion

timber harborBOT
#
xananax#0

Preview:```ts
...
export function GodotShortcut<
T extends TypeKeys<S>,
S extends ScopeKeys = "general"

({scope, type}: {scope?: S; type: T}) {
const scopeDict = MAP[scope ?? "general"]
const mapValue = scopeDict[
type as keyof typeof scopeDict
] as ShortcutsMap[S][T]
const hasMac = "mac" in mapValue
const defaultShortcut = mapValue.default
console.log(mapValue)
}
...```

torn zephyr
#

When I get the leaf object (the one with {default, mac?}), typescript isn't managing to know what the object is

#

As a result, the two lines:

  const hasMac = 'mac' in mapValue
  const defaultShortcut = mapValue.default

give me the error:

Property 'default' does not exist on type '{ readonly general: { readonly "open 2d editor": { readonly default: "ctrl+f1"; readonly mac: "opt+1"; }; readonly "open 3d editor": { readonly default: "ctrl+f2"; readonly mac: "opt+2"; }; readonly "open script editor": { ...; }; ... 25 more ...; readonly "expand bottom panel": { ...; }; }; ... 11 more ...; readonl...'

odd shard
#

I might be able to help if you can reduce the example down to 10 lines. I'm not in the mood to sift through all that now though, sorry.

torn zephyr
#

The important part is really the function, the data object follows the signature I wrote at the top, the amount of items is not relevant. But ok, give me a min

vocal warren
#

Turn it into a DU instead:

timber harborBOT
#
xananax#0

Preview:```ts
const MAP = {
general: {
"open 2d editor": {
default: "ctrl+f1",
mac: "opt+1",
},
"close scene": {
default: "ctrl+shift+w",
mac: "cmd+shift+w",
},
},
debugger: {
continue: {default: "f12"},
},
} as const

type ShortcutsMap = typeof MAP
...```

#
nonspicyburrito#0

Preview:ts const MAP = { general: { "open 2d editor": { default: "ctrl+f1", mac: "opt+1", }, "open 3d editor": { default: "ctrl+f2", mac: "opt+2", }, "open script editor": { default: "ctrl+f3", mac: "opt+3 ...

torn zephyr
#

Oh thanks!

#

This is so puzzling though, why does it work

vocal warren
#

!hb discriminated union

timber harborBOT
vocal warren
#

The code just generates the DU from your MAP.

torn zephyr
#

Excellent technique

#

Thanks!

#

... But it still infers the mapValue as any or never:

timber harborBOT
#
xananax#0

Preview:ts const MAP = { general: { "open scene": {default: "ctrl+o", mac: "cmd+o"}, "close scene": { default: "ctrl+shift+w", mac: "cmd+shift+w", }, }, debugger: { "step into": {default: "f11"}, "step over": {default: ...

vocal warren
#

It's because of:

#

!:corr*

timber harborBOT
#
retsam19#0
`!retsam19:correspondence-problem`:

There's a particular pattern that is safe but hard for the Typescript compiler to handle, which I call the "correspondence problem":

const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo" },
  { func: (arg: number) => {}, arg: 0 },
];

for (const { func, arg } of functionsWithArguments) {
  func(arg);
//     ^^^
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
//   Type 'string' is not assignable to type 'never'.
}

The problem is that func is typed as (x: string) => void | (x: number) => void and arg is string | number, but the compiler can't prove that they "correspond": that, for example, arg is only a string when func accepts strings.

As far as the type are concerned, arg could be number, and func could be (arg: string) => void, and that would be a type-error. It's easy for us to see that that won't happen, but that requires understanding the program at a higher-level than the level the compiler operates.

Depending on the specifics there's sometimes clever fixes, but usually I recommend using a type assertion and ignoring the issue:

func(arg as never);
vocal warren
#

Imo the best way is to just have a type for your shortcuts, then cast the correspondence problem.

torn zephyr
#

Yup, this works:

timber harborBOT
#
xananax#0

Preview:ts const MAP = { general: { "open scene": {default: "ctrl+o", mac: "cmd+o"}, "close scene": { default: "ctrl+shift+w", mac: "cmd+shift+w", }, }, debugger: { "step into": {default: "f11"}, "step over": {default: ...

torn zephyr
#

Thanks!

vocal warren
#

I'd personally do it like this:

timber harborBOT
#
nonspicyburrito#0

Preview:ts const MAP = { general: { 'open scene': { default: 'ctrl+o', mac: 'cmd+o' }, 'close scene': { default: 'ctrl+shift+w', mac: 'cmd+shift+w' }, }, debugger: { 'step into': { default: 'f11' }, 'step over': { default: 'f10' }, ...

torn zephyr
#

I'm going to ask, but you've already helped me a lot so do not feel obligated at all to answer, and thanks a lot already. But isn't as never supposed to mean that this is a situation that cannot happen? How can this index the object, and why isn't any working instead?

vocal warren
#

as never is just a slightly safer alternative to as any.

torn zephyr
#

At any rate, thanks a lot, I consider this problem solved. Is there any official way of marking it as such?

vocal warren
#

Use !resolved.