#How to get keys from a union of objects?

89 messages · Page 1 of 1 (latest)

proven dragon
#

I don't understand a trivial thing.
Why keyof Selector is returning never?

type Selector = {
  str: string;
} | {
  num: number;
};

#

It should be exactly "str" | "num", no?

agile thicket
#

keyof gives all guaranteed keys

#

str might not exist, num might not exist, so you get an empty union back

proven dragon
#

but they together must exist

agile thicket
#

if you want all possible keys for each member of the union, you have to distribute

proven dragon
#

either one or another

agile thicket
#

but you don't know which, that's the issue

proven dragon
#

Ok, then how in this case I can get those keys?
I need it for a function.
The function takes key and returns corresponding type

#

!ts

dapper surgeBOT
#
type GetKeys<T> = keyof T

type Selector = {
  str: string;
} | {
  num: number;
};

type D = GetKeys<Selector>

function foo<K extends keyof Selector>(sel: K): Selector[K] {
  if (sel === "num") {
    return 1;
//  ^^^^^^
// Type 'number' is not assignable to type 'Selector[K]'.
//   Type 'number' is not assignable to type 'never'.
  }
  if (sel === "stre") {
    return 1;
//  ^^^^^^
// Type 'number' is not assignable to type 'Selector[K]'.
  }
  throw new Error();
}
proven dragon
#

Of course this doesn't work, but it doesn't work with & as well:

dapper surgeBOT
#
onkeltem#0

Preview:```ts
type Selector = {
str: string
} & {
num: number
}

function foo<K extends keyof Selector>(
sel: K
): Selector[K] {
if (sel === "num") {
return 1
}
if (sel === "stre") {
return 1
}
throw new Error()
}```

agile thicket
#

you have a typo there, but if the key was str wouldn't you want to return a string?

#

im not sure what you're trying to achieve there

proven dragon
#

yes, a typo sorry

dapper surgeBOT
#
onkeltem#0

Preview:```ts
type Selector = {
str: string
} & {
num: number
}

function foo<K extends keyof Selector>(
sel: K
): Selector[K] {
if (sel === "num") {
return 1
}
if (sel === "str") {
return "asd"
}
throw new Error()
}```

proven dragon
#

I try to return the type determined by the "sel" param

agile thicket
#

if Selector is just being used to map then it would be a single object rather than a union

proven dragon
#

But K is of "str" | "num" type

agile thicket
#

this is a case of not being able to narrow generics im pretty sure

#

but this kinda feels like xyproblem, again, what are you trying to achieve here?

proven dragon
#

So there is no way to write one function I have to double the code

agile thicket
#

that's not what i said

proven dragon
#
type A = {
  fetch: FetchConfig
  generate: GenerateConfig
}

export function readConfig<K extends keyof A>(
  type: K,
  overrides: A[K],
  configFilepath?: string
): A[K] {
  const configData = { ...overrides }
  if (configFilepath != null) {
    logger.debug(`Reading config from ${configFilepath}`)
    Object.assign(configData, JSON.parse(fs.readFileSync(configFilepath).toString()))
  }
  logger.debug(`Validating config`)
  if (type === 'fetch') {
    return fetchConfig$.assert(configData)
  }
  return generateConfig$.assert(configData)
}
#

The only difference is what validator to use - either fetchConfig$ or generateConfig$

#

the rest is identical

agile thicket
#

yeah that won't work from the signature directly since K can be a union

proven dragon
#

I cannot do this as well:

type A = {
  fetch: FetchConfig
  generate: GenerateConfig
}

const validators = {
  fetch: fetchConfig$,
  generate: generateConfig$,
}

export function readConfig2<K extends keyof A>(
  type: K,
  overrides: PartialOrUndefined<A[K]>,
  configFilepath?: string
): A[K] {
  const configData = { ...overrides }
  if (configFilepath != null) {
    logger.debug(`Reading config from ${configFilepath}`)
    Object.assign(configData, JSON.parse(fs.readFileSync(configFilepath).toString()))
  }
  logger.debug(`Validating config`)
  return validators[type].assert(configData)
}

because it returns a union

agile thicket
#

you could have it be generic over the validators instead

#

could you make a reproducable example in the playground

fair copper
#

Can you make a TS playground?

#

The classic way to solve Obj[K] as return type is to just make the logic also obj[key]:

dapper surgeBOT
#
type Selector = {
    str: string
    num: number
}

function foo<K extends keyof Selector>(sel: K): Selector[K] {
    return {
        str: 'foo',
        num: 42,
    }[sel]
}
fair copper
#

It might be possible to rewrite your actual code into this.

agile thicket
#

needs 2 arguments to match here, so probably going to have to rewrite the arguments to not have that

#

or distribute, i guess

#

feels like rewriting would be easier though

fair copper
#

I generally consider condtional return type an anti pattern anyways, I'd much rather splitting the function into two.

#

Especially since you are already basically writing the code twice by having two if checks.

proven dragon
#

I'm trying to do that

proven dragon
fair copper
#

You can move the assert into the map.

proven dragon
#

I already did yeah:

const validators = {
  fetch: fetchConfig$,
  generate: generateConfig$,
}
fair copper
#

Can't say without a TS playground reproduction, but also like I said, I'd much prefer splitting the function.

proven dragon
#

Ok, I'll try to do it. But actually I had them splitted. Still had extra code

fair copper
#
const fooResult = fn('foo', fooArg)
const barResult = fn('bar', barArg)
const fooResult = fooFn(fooArg)
const barResult = barFn(barArg)
#

The only thing you gain out of combining the two functions is just, one less import.

#

If the two functions have some duplicated code, you can abstract them just like any other code.

proven dragon
#

Sandbox:

dapper surgeBOT
#
onkeltem#0

Preview:```ts
import {type} from "arktype"
import {fs} from "node"

const fetchConfig$ = type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
})

const generateConfig$ = type({
name: "string>0"
...```

proven dragon
#

I mean, this is the starting point: two similar functions

#

They differ by 1) interface (parameters) and 2) output

#

The middle part however is similar identical

#

That's why I was trying to make just one function that takes two different inputs and produces two different outputs

crimson tusk
agile thicket
proven dragon
# dapper surge

the order of the overriding is wrong, but that's because I copied some outdate code, a moment...

fair copper
#

It's very simple:

function commonLogic() {
    // do
    // something
    // similar
    // in
    // both
}

function fooFn(fooArg) {
    commonLogic()
    doSomethingSpecificToFoo(fooArg)
}

function barFn(barArg) {
    commonLogic()
    doSomethingSpecificToFoo(barArg)
}
proven dragon
#

Maybe I'd still prefer a type mangling way, because extracting this part would make more code

fair copper
#

That's completely not true.

proven dragon
#

So this is my starting point then:

dapper surgeBOT
#
onkeltem#0

Preview:```ts
import {type} from "arktype"
import {fs} from "node"

const fetchConfig$ = type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
})

const generateConfig$ = type({
name: "string>0"
...```

#
that_guy977#0

Preview:```ts
import {type, Type} from "arktype"

const fetchConfig$ = type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
})

const generateConfig$ = type({
name: "string>0",
specFilepath: "string>0"
...```

agile thicket
#

something like that?

dapper surgeBOT
#
nonspicyburrito#0

Preview:```ts
import {type} from "arktype"
import fs from "node:fs"

const fetchConfig$ = type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
})

const generateConfig$ = type({
name: "string
...```

fair copper
proven dragon
#

Result:

dapper surgeBOT
#
onkeltem#0

Preview:```ts
import {type} from "arktype"
import {fs} from "node"

const fetchConfig$ = type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
})

const generateConfig$ = type({
name: "string>0"
...```

proven dragon
#

The message is repeating... as well as data retreival

agile thicket
fair copper
#

What message is repeating? Anything repeating you can just move that into the common logic.

proven dragon
# dapper surge

Yeah, I see. I was thinking about passing validator as parameter, but I was trying to avoid it because then I would have to export the schemas and use them outside of my file, while I was trying to keep specifities about reading and validating config just here - in config.ts

proven dragon
#

I can technically put it inside the "common part", but that guy is responsible for reading config file and merging only. It would be strange to have it messaging about validation 🙂

fair copper
#

There's nothing stopping you from renaming that to something else like preprocess.

#

But if you really really want to combine both functions, here's a solution:

dapper surgeBOT
#
nonspicyburrito#0

Preview:```ts
import {type} from "arktype"
import fs from "node:fs"

const configMap = {
fetch: type({
name: "string>0",
projectId: "number>0",
fetchDirpath: "string>0",
apiPath: "string>0",
ref: "string>0",
}),
ge
...```

fair copper
#

In this case it might've been possible to combine both, but that's not something you should take for granted for every case.

fair copper
fair copper
proven dragon
#

Ok, I need to investigate the difference

agile thicket
#

you had your config types and the validator separated before

proven dragon
#

true