#Infering type from generic object

41 messages · Page 1 of 1 (latest)

tame ravine
#

Hello everybody!

Kind of stuck on what feels like a somewhat tricky issue:
I define the shape of a config object we use in our company using generics, then I want to provide functions to savely retrieve keys and types of their values from that config.

An example config would look like this:

const config = defineFlagConfig({
    "core": {
        globalConstants: {
            "test": 5
        }
    },
    "override": {
        globalConstants: {
            test: 10,
            "test-2": 20
        }
    }
})

I then have functions to type safely merge them and retrieve keys.

However I'd like to now only allow putting numbers in there, but also strings and arrays of strings. When accessing a key that holds a string I'd like ts to infer the type properly as a string and not as string | number

I created a tiny reproduction on the playground: See next message

So adding

globalConstants: {
            test: 10,
            "test-2": 20
            "string-test": "my-string"
        }

and accessing it should type as follows:

const constant = getGlobalConstantValue("string-test" , merged) -> infered as string
const constant = getGlobalConstantValue("test" , merged) -> infered as number

Thank you!

bitter fractalBOT
#
warflash#0

Preview:```ts
type FlagConfig = {
globalConstants: Record<string, number>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

export function defineFlagConfig<T extends NamespacedFlagConfig>(config: T): T {
return config
}

// /**
// * Internal function to fetch global constants
...```

tame ravine
#

!helper would appreciate some insights whether this is doable or whether I'm on the wrong path here 🙏

haughty harbor
#

sounds like satisfies would help

tame ravine
#

Where would you recommend making use of satisfies here?

haughty harbor
#

nvm

#

it seems to be working already

#

just changed ```diff
type FlagConfig = {

  • globalConstants: Record<string, number>
  • globalConstants: Record<string, number | string | string[]>
    }
#

do you want it to know that the output type is 10 rather than number? that is possible

tame ravine
# haughty harbor it seems to be working already

okay that's crazy. In the playground that indeed works, in my workspace it doesn't. It seems like another key that I allow setting on the config is what's screwing it up. Let me create a playground real quick

bitter fractalBOT
#
balam314#0

Preview:```ts
type FlagValue = number | string | string[];

type FlagConfig = {
globalConstants: Record<string, FlagValue>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

export function defineFlagConfig<const T extends NamespacedFlagConfig>(config: T): T {
return config
...```

tame ravine
#

Updated the playground to reflect the issue:

bitter fractalBOT
#
warflash#0

Preview:```ts
type FlagConfig = {
globalConstants: Record<string, number | string>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

function defineFlagConfig<T extends NamespacedFlagConfig>(config: T): T {
return config
}

// /**
// * Internal function to fetch global constants
...```

tame ravine
#

It seems the wrapper useGlobalConstant I provide if screwing something up. The primitive getGlobalConstantValue works fine like you mentioned 🤔

haughty harbor
#

ok I found the issue

#

(in hindsight it's glaringly obvious but I was preoccupied with fixing the errors)

#

useGlobalConstant isn't generic

#

it needs to be generic

#

Fixed!

bitter fractalBOT
#
balam314#0

Preview:```ts
type FlagConfig = {
globalConstants: Record<string, number | string>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

function defineFlagConfig<const T extends NamespacedFlagConfig>(config: T): T {
return config
}

// /**
// * Internal function to fetch global constants
...```

haughty harbor
#

might want to clean it up a bit

#

it works!

#

@tame ravine is this what you wanted

tame ravine
#

Yes that looks absolutely perfect

tame ravine
haughty harbor
#

I almost didn't lol

tame ravine
#

It felt super natural just using the keyof instead of a generic since we're dealing with an already merged and instantiated object but yeah. makes sense now :)

haughty harbor
#

just realized, I should probably make it forbid configs of the same key being different types

tame ravine
#

that would be handy if that worked, true

haughty harbor
#

well the error is in the wrong place...

#

eh

#

it should really be at const config =

bitter fractalBOT
#
balam314#0

Preview:```ts
type FlagConfigValue = number | string;
type FlagConfig = {
globalConstants: Record<string, FlagConfigValue>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

function defineFlagConfig<const T extends NamespacedFlagConfig>(config: T): T {
return config
...```

tame ravine
#

That works nicely yep, thanks 👌
Ideally it'd error right at the config object but "never" works as a middleground solution.
Went ahead and added a error description to make it a bit more obvious what's happening

type TypeName<T> = T extends string
    ? "string"
    : T extends number
    ? "number"
    : T extends boolean
    ? "boolean"
    : T extends undefined
    ? "undefined"
    : T extends object
    ? "object"
    : "unknown"

type MergeWithOverrideError<A, B> = A extends B
    ? B
    : `Error: Incompatible property types '${A}' of type ${TypeName<A>} and '${B}' of type ${TypeName<B>} cannot be merged`

type MergeOverride<A extends Record<string, unknown>, B extends Record<string, unknown>> = Omit<A, keyof B> &
    Omit<B, keyof A> & {
        [P in keyof A & keyof B]: A[P] extends string
            ? B[P] extends string
                ? B[P]
                : MergeWithOverrideError<A[P], B[P]>
            : A[P] extends number
            ? B[P] extends number
                ? B[P]
                : MergeWithOverrideError<A[P], B[P]>
            : never
    }
tame ravine
#

!resolved

tame ravine
# haughty harbor

peek It's me again, hope you don't mind.
I ran into this weird edgecase where merging a namespace with itself works fine when the value is just a number or string. If I make them Records however it just returns never. Do you happen to have any idea on that? Thanks!

bitter fractalBOT
#
warflash#0

Preview:```ts
type FlagConfigValue = number | string | Record<string, number>
type FlagConfig = {
globalConstants: Record<string, FlagConfigValue>
}

type NamespacedFlagConfig = Record<string, FlagConfig>

function defineFlagConfig<const T extends NamespacedFlagConfig>(config: T): T {
...```