#Nested related types

58 messages · Page 1 of 1 (latest)

lone lake
#

I'm trying to define my error api responses in a single location as to avoid duplicating code (source code and potentially re-using error codes to mean different things in different places).

I'm trying to create a function that takes in the error class key first, then requires the error classes code key and then a value of the same type as the data element. I can get the first argument to work, but the second seems to always be never

worn escarpBOT
#
bnason#0

Preview:ts export const ApiErrors = { INTERNAL: { class: "00", description: "Internal Server Error", codes: { DEFAULT: { code: "00", description: "Internal Server Error", data: {eventId: ""}, }, }, }, UNAUTHORIZED: { class: "01", description: "Unauthorized", codes: { INCORRECT: { ...

acoustic dome
#

i think you're confused about how typeof works. keyof typeof ApiErrors is 'INTERNAL' | 'UNAUTHORIZED', so that is the type of error. when you later do typeof error you will always get back 'INTERNAL' | 'UNAUTHORIZED' regardless of the specific runtime value of error

#

you want to use type parameters instead

#

!hb generics

worn escarpBOT
lone lake
#

Right, i'm trying to use typeof error to narrow down ApiErrors to the one that is passed in

#

I'm not sure how a generic parameter would help here. This isn't operating on anything that is generic really. Just the one big ApiErrors object

craggy fern
#

makeApiErrorResponse would be generic.

#

You're trying to link the type of the first param with the other params - that's a classic use of generics.

lone lake
#

I don't know how to do the deeply nested type linking. This is what I get when I try to turn it into a generic, but it still doesn't work.

worn escarpBOT
#
bnason#0

Preview:ts export const ApiErrors = { INTERNAL: { class: "00", description: "Internal Server Error", codes: { DEFAULT: { code: "00", description: "Internal Server Error", data: {eventId: ""}, }, }, }, UNAUTHORIZED: { class: "01", description: "Unauthorized", codes: { INCORRECT: { ...

craggy fern
#
export const makeApiErrorResponse = <E extends keyof typeof ApiErrors>(
    error: E,
    code: keyof (typeof ApiErrors)[E]['codes'],
) => {}

Your generic was saying that the generic extended the whole ApiErrors object but you want to pick a key from it instead.

lone lake
#

Ah ok, I needed to be more specific with my generic lol

#

ok now let me see if I can get data included as well

lone lake
#

ugh this is all i've got so far....

worn escarpBOT
#
bnason#0

Preview:ts export const ApiErrors = { INTERNAL: { class: "00", description: "Internal Server Error", codes: { DEFAULT: { code: "00", description: "Internal Server Error", data: {eventId: ""}, }, }, }, UNAUTHORIZED: { class: "01", description: "Unauthorized", codes: { INCORRECT: { ...

craggy fern
#

Not all of your codes have a data field so you can't just do ["data"]

#

It looks like this works:

#
type ExtractDataIfPossible<T> = T extends { data: infer Data } ? Data : undefined
export const makeApiErrorResponse = <
    E extends keyof typeof ApiErrors,
    C extends keyof (typeof ApiErrors)[E]['codes']
>(
    error: E,
    code: C,
    data?: ExtractDataIfPossible<typeof ApiErrors[E]['codes'][C]>
) => {}
#

(Though it errors because data: { eventId: '' } is causing eventId to only allow '' )

lone lake
#

yea I wasn't sure how else to encode the data type into each error, so I just provided an "empty"
object

#
type ExtractDataIfPossible<T> = T extends { data: infer Data } ? Data : undefined

export const ApiErrors = {
    INTERNAL: {
        class: '00',
        description: 'Internal Server Error',
        codes: {
            DEFAULT: { code: '00', description: 'Internal Server Error', data: { eventId: '' } satisfies { eventId: string } },
        },
    },
    UNAUTHORIZED: {
        class: '01',
        description: 'Unauthorized',
        codes: {
            INCORRECT: { code: '00', description: 'Incorrect credentials' },
            EXPIRED: { code: '01', description: 'Expired' },
        },
    },
} as const

export const makeApiErrorResponse = <
    E extends keyof typeof ApiErrors,
    C extends keyof (typeof ApiErrors)[E]['codes']
>(
    error: E,
    code: C,
    data?: ExtractDataIfPossible<(typeof ApiErrors)[E]['codes'][C]>,
) => {}

const error = makeApiErrorResponse('INTERNAL', 'DEFAULT', { eventId: 'asd' })
#

seems to work if I cast the data object

craggy fern
#

I guess a simple tweak to fix the '' issue would be:

const anyString: string = '';
//... { eventId: anyString }
lone lake
#

ok now to figure out how to get data to be required when it was provided hmm

craggy fern
#

That's... going to be a pain.

#

TBH the easiest thing is to just drop the ? from data.

#

And force makeApiErrorResponse('UNAUTHORIZED', 'INCORRECT', undefined) instead of makeApiErrorResponse('UNAUTHORIZED', 'INCORRECT')

lone lake
#

yea =\

craggy fern
#

I guess this works, but... 🤔

#
type ExtractDataIfPossible<T> = T extends { data: infer Data } ? WidenValues<Data> : undefined
type WidenValues<T> = {
    [K in keyof T]: T[K] extends string ? string : T[K]
}

export const makeApiErrorResponse = <
    E extends keyof typeof ApiErrors,
    C extends keyof (typeof ApiErrors)[E]['codes']
>(
    error: E,
    code: C,
    ...args: ExtractDataIfPossible<typeof ApiErrors[E]['codes'][C]> extends undefined ? [] : [ExtractDataIfPossible<typeof ApiErrors[E]['codes'][C]>]
) => {}
#

(WidenValues is an alternative to the anyString thing)

worn escarpBOT
#
const f = (a: string, b: void) => {}
f('no error')
craggy fern
#

Ah neat, not familiar with that trick

acoustic dome
#

yeah it's a weird one that is honestly rarely useful. i thought we might finally have a use case here but nope 😆

#

this is maybe a bit better than your suggestion though:

worn escarpBOT
#
mkantor#0

Preview:```ts
...
type DataArgIfNecessary<T> = T extends {
data: infer Data
}
? [data: WidenValues<Data>]
: []
type WidenValues<T> = {
[K in keyof T]: T[K] extends string ? string : T[K]
}

export const makeApiErrorResponse = <
E extends keyof typeof ApiErrors,
C extends keyof typeof ApiErrors[E]["codes"]

(
error: E,
code: C,
...rest: DataArgIfNecessary<
typeof ApiErrors[E]["codes"][C]

) => {}
...```

craggy fern
#

Ah yeah, a good cleanup

lone lake
#

Thank you! I doubt I would have ever figured all that out lol...

#

ok now trying to actually use the code to return data is giving me this Type 'C' cannot be used to index type

#
export const makeApiErrorResponse = <E extends keyof typeof ApiErrors, C extends keyof (typeof ApiErrors)[E]['codes']>(
    error: E,
    code: C,
    ...data: DataArgIfNecessary<(typeof ApiErrors)[E]['codes'][C]>
) => {
    const apiError = ApiErrors[error]
    const apiErrorCode = apiError.codes[code]

    return { error: `E${apiError.class}C${apiErrorCode.code}`, data }
}
craggy fern
#

Yeah, there's probably going to be some casting involved in making this work

acoustic dome
#

@lone lake are you wedded to this particular runtime representation of ApiErrors? i suspect your life might be easier if you can flatten it

lone lake
#

not necessarily, was just hoping to not have to hard code the same E00 stuff everywhere, but shrug is it worth it? lol

acoustic dome
#

here's a different take:

worn escarpBOT
#
mkantor#0

Preview:```ts
const ApiErrors = {
"INTERNAL DEFAULT": "E00C00",
"UNAUTHORIZED INCORRECT": "E01C00",
"UNAUTHORIZED EXPIRED": "E01C01",
} as const

type ErrorData = {
"INTERNAL DEFAULT": {eventId: string}
}

export const makeApiErrorResponse = <
E extends keyof typeof ApiErrors

(
errorName: E,
...[data]: E extends keyof ErrorData
? [data: ErrorData[E]]
: []
) => ({
error: ApiErrors[errorName],
data,
})
...```

lone lake
#

Yea I don't love it, but I'm not sure what exactly I want to do since I can't fully seem to resolve it. I cleaned up my types to try to make it more readable and this is what I have. Technically it works, it just gives a single type error in the makeErrorResponse function, which i might just have to live with.

worn escarpBOT
#
bnason#0

Preview:```ts
const errors = {
ERROR1: {
id: '00',
codes: {
CODE1: {
id: '00',
data: { incidentId: '' } satisfies { incidentId: string },
}
} as const
},
ERROR2: {
id: '01',
codes: {
CODE1: {
id: '00',
}
} as const
}
} as const

type Errors = keyof typeof errors
...```

craggy fern
#

Yeah, would probably just const code = codes[codeName as never] to make that error go away and live with it - might be possible to fix, might not, might not be worth it.

lone lake
#

I just did // @ts-ignore ha, at least I can search for that in the future and see that I purposfuly ignored it

craggy fern
#

@lone lake Better to cast.

#

// @ts-ignore is meant to be a feature of last-resort - it can sometimes suppress more than you want and makes it harder to see what the actual error is.

#

Casting can be more used as a scapel where you tweak one type, rather than just suppressing errors on a particular line.

acoustic dome
#

if you really don't like the type assertion you could do this:

worn escarpBOT
#
mkantor#0

Preview:ts ... const codes: Record<PropertyKey, { id: string }> = error['codes'] ...