#How to get keys from a union of objects?
89 messages · Page 1 of 1 (latest)
keyof gives all guaranteed keys
str might not exist, num might not exist, so you get an empty union back
but they together must exist
if you want all possible keys for each member of the union, you have to distribute
either one or another
but you don't know which, that's the issue
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
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();
}
Of course this doesn't work, but it doesn't work with & as well:
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()
}```
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
yes, a typo sorry
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()
}```
I try to return the type determined by the "sel" param
if Selector is just being used to map then it would be a single object rather than a union
But K is of "str" | "num" type
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?
So there is no way to write one function I have to double the code
that's not what i said
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
yeah that won't work from the signature directly since K can be a union
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
Yeah 😦
you could have it be generic over the validators instead
could you make a reproducable example in the playground
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]:
type Selector = {
str: string
num: number
}
function foo<K extends keyof Selector>(sel: K): Selector[K] {
return {
str: 'foo',
num: 42,
}[sel]
}
It might be possible to rewrite your actual code into this.
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
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.
I'm trying to do that
this specific one won't work in my real code as I need to perform one assert at a time
You can move the assert into the map.
I already did yeah:
const validators = {
fetch: fetchConfig$,
generate: generateConfig$,
}
Can't say without a TS playground reproduction, but also like I said, I'd much prefer splitting the function.
Ok, I'll try to do it. But actually I had them splitted. Still had extra code
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.
Sandbox:
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"
...```
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
would you mind comin here?
https://discord.com/channels/508357248330760243/1057653400046674112/threads/1244984837543165963
Discord is the easiest way to communicate over voice, video, and text. Chat, hang out, and stay close with your friends and communities.
please do not advertise your own question in other people's threads
the order of the overriding is wrong, but that's because I copied some outdate code, a moment...
How do you abstract this:
function fooFn(fooArg) {
// do
// something
// similar
// in
// both
doSomethingSpecificToFoo(fooArg)
}
function barFn(barArg) {
// do
// something
// similar
// in
// both
doSomethingSpecificToFoo(barArg)
}
It's very simple:
function commonLogic() {
// do
// something
// similar
// in
// both
}
function fooFn(fooArg) {
commonLogic()
doSomethingSpecificToFoo(fooArg)
}
function barFn(barArg) {
commonLogic()
doSomethingSpecificToFoo(barArg)
}
Maybe I'd still prefer a type mangling way, because extracting this part would make more code
That's completely not true.
So this is my starting point then:
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"
...```
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"
...```
something like that?
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
...```
It's just applying this refactor.
Result:
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"
...```
The message is repeating... as well as data retreival
@proven dragon have you checked this out
What message is repeating? Anything repeating you can just move that into the common logic.
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
The console.log('Validating data'); message which is part of logging
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 🙂
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:
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
...```
In this case it might've been possible to combine both, but that's not something you should take for granted for every case.
Really you need to understand this refactor. "I have two pieces of similar code how do I make it DRY" is a very very common task in programming.
The solution I gave uses exactly this trick.
Strange, I was trying to make it from the beginning but in my setup it didn't work...
Ok, I need to investigate the difference
you had your config types and the validator separated before
true