#Help with my Union Mapper

42 messages · Page 1 of 1 (latest)

blissful quail
#

I am attempting to create a type where it is keyed off of the type field of a union, so I can cast it (I know that sucks), but I think necessary due to my need to loop through a discriminated Union and act on the different specific discriminated instance. So, I have this mapping type working, but I want to make it more flexible for my helper lib:

// this works
type UnionMap<T extends { 'type': V }, V extends PropertyKey, F = 'type'> = {
[K in T['type']]: Extract<T, { 'type': K }>
};

//Would like this to work: F cannot be used to index T it says.
type UnionMap<T extends { F: V }, V extends PropertyKey, F extends PropertyKey = 'type'> = {
[K in T[F]]: Extract<T, { F: K }>
};

Would like to replace 'type' with some Generic type var F. I could not figure out how, it keeps thinking T[F] does not return a PropertyKey, even though above I have T extending { F: V }

orchid grove
#

could you elaborate on your usecase? the way you've phrased it, seems like it might be an xyproblem

blissful quail
#

sure, I have a discriminated union with a field type: string, and a few types of this union

#

and i have an object w/ a few of these, and I need to pass them downstream based on their real specific type

#

so, basically, iterate through, cast them, and feed them downsteam, and I hate to cast them, but can't seem to do it any other way

orchid grove
#

anyways the issue is that { F: V } (and also { F: K }) use F as the name, they don't take the F type, you'd need a mapped type or use Record<F, V>/Record<F, K> as a shortcut

orchid grove
#

how are you using them?

blissful quail
#

T extends { F: V } <--- isn't the V the value, is this the same as saying extends Record<F, V> ?

orchid grove
blissful quail
#

oh?

orchid grove
#

{ F: V } -> { "F": V }, that might make it clearer

#

just like js objects

blissful quail
#

oh, interesting

#

ok, back to your higher level question

#

so, i read about a limitation of typescript, called corellated unions

#

i think this is my issue

#

say I have an array of these unions, and i want to do a forEach on them

#

zoneConfig.zones.forEach((zone) => {

#

zone is the union of 3 possible concrete types

#

without something like If (zome.type === 'Alarm') then { (narrowed here) }

#

it won't ever narrow, as type erasure happens at runtime

orchid grove
#
const key = "x"
const obj = { key: 0 };
obj.key          // 0
obj["key"]       // 0
obj[key]         // undefined
const computed = { [key]: 1 };
computed.key     // undefined
computed["key"]  // undefined
computed[key]    // 1

type K = "x";
type Obj = { K: 0 };
Obj[K]           // error
Obj["K"]         // 0
type Computed = Record<K, 1>; // or { [P in K]: 1 }
Computed[K]      // 1
Computed["K"]    // error
orchid grove
#

type erasure means you can't read or process the types, but zone.type is not a type, it is a value

#

(well, narrowing doesn't exist at runtime, but you still get diverging logic)

#

having to cast means types still matter, it's not a type erasure issue. you have issues at compiletime, not runtime

orchid grove
#

could you make an example of your usecase on the playground?

blissful quail
#

ok

#

will do

magic whaleBOT
#
mwsailing_52679#0

Preview:```ts
const Zones = ["Alarm", "Fire", "Perimeter"] as const
type ZoneTypes = typeof Zones[number]

const SensorRoles = ["immediate", "delayed"]
type SensorTypes = typeof SensorRoles[number]

interface SensorBase {
id: number
role: SensorTypes
}

interface AlarmSensor extends SensorBase {
...```

blissful quail
#

so, the idea is that my config structure is modeled in a way that prevents you from creating a Fire zone w/ incorrect sensor in the config, which is great. But now, i can't use this config to create the right 'machines'

#

well, of course I can.... but not in a typescript way without the 'as' keyword

orchid grove
#

sorry, was in class, i'll take a look now

#

this is the correspondence problem, and you could use an as never here instead of making any complex logic

#

!:corr%

magic whaleBOT
#
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);
orchid grove
#

more specifically, ts can't deduce m by z

#

this is unrelated to type erasure

blissful quail
#

nice, thank you!