#Fixing typescript errors when using results in the loader

1 messages · Page 1 of 1 (latest)

stuck cobalt
#

Hello everyone!

In my larger codebase, I employ the usage of Results<T, E> and some functional programming everywhere, to improve the error handling for my users and myself.

To make use of neverthrows classes beyond the network boundary, I have some helper for serializing and hydrating result classes:

export function hydrateSerializedResult<TData, TError>(
  result: SerializedResult<TData, TError>
): Result<TData, TError> {
  if (result.type === RESULT_TYPES.SUCCESS) {
    return ok(result.data);
  }

  return err(result.error);
}

export function serializeSuccess<T>(data: T): SerializedResult<T, never> {
  return { data, type: RESULT_TYPES.SUCCESS };
}

export function serializeError<T>(error: T): SerializedResult<never, T> {
  return { error, type: RESULT_TYPES.ERROR };
}

Errors always share the same base, (with a code) but they can differ in their properties (for example a not found error has an ID on it, to trace that later, or a validationError has a list of invalid fields on it)

When using early returns in the loader, for example

const firstResult = await doSomething()
if (firstResult.isErr()) {
  return serializeError(firstResult.error)
}
const secondResult = await doSomethingElse()
if (secondResult.isErr()){
  return serializeError(secondResult.error)
}
return serializeSuccess(secondResult.value)

Now typescript complains that the both error types (despite sharing the same base) are not assignable to each other when passing the loaderData in the serializeResult function.

Is there any type-level magic I can apply here to tell TS (without manually specifying a union of possible errors) that this will infact work?

Thanks in advance!

stuck cobalt
# fiery rose I just threw this together (I made some assumptions about the general structure ...

Thanks for starting,

As you can see the problem is not with receiving the type, but rather "hydrating" it back to a usable result (https://www.typescriptlang.org/play/?#code/MYewdgzgLgBASgUQMoFUAyAVA+hgmgBWRgF4YBvAKBmplQGE7kkAuAIggFdhgBTCCVgBoqNBHDgB5OGx4AnWSFmsKAXxgBDCDFCQoAbgoUoATwAOPWnICW6gDZWAXjwAmSLr34AeDAD4S5EWpndSh1ZgwDGhgTc2YYnhAAM3hkdGw8QiQAbVZ6RiQkVgBdAxVDeMtZG3snZwR5RW8-UkoouQVZcMiaeLizBOTEVEwcAmQcsUk4YtLDYFtNLQkAayaAqIA3Ow4eZhgMQJgrCBWACgBKPahZHfWomlkeKA5ZMGibnkOyqJ1oG+AoIpTltbDs9hhznd7tEABbHAB0INupCRnyiZTKFHmixg9VkngQflaNHaij2CEOxzOlxgiTsEAsxOhj2er1p9J43Wo3xov2uXEBslOpM6uMhTPuUDhEHhIv8Iq+qkMRn6lWqjhccD4HFsUE8ABFBIT-EhrHYNa53HwIAafAAfU1Vc21PGNQkGCg8AAepkUsESHDAAKs4BgDKdNR4bm41qap2CoXBNMd6tqWs4uu8ghgYB4GzkRJELJebzIMAT6mzvRSw3SYyQ8LyTBgKlmKvMMAAQpoeK7ZP4JaBnLswBwALYAIzkbe9vtk-sDwdD4dTvYa+IwMG9UB4YGcWm7DL7PmF66TexTzs12szufzsmzvjuxbZZZFVf6eyGaVGmXhkykFs2wAemAhAxx1EILAAd1kdRTFMKwwAAc1hCxHgzWAkLDEAxyeOEUM9H0-VpRcoBDN4YWMZw4J3dMdT1DBs0JU4kNMDgoAvM1I2cejMyY3EfHFSlklYsB2KgeEKmIGSax-DJkEbFAGCYYToWoF83lzaCYDONiOPhCtzkVKJNJzHgdLxMSJNlddjMMTEsXAaAYFsEB1GHftSE0YwgxgC5iB8CUrFEgBZEIYXhOC91wi4YE8GAAAZ4QAVmM9SYDMlcrz7U4yCHXYAEYACYAGYABZBDHCBkLYCBcIsEVWBUc4uSOMKIqi9QYrHOKEuStLDlMp4SzDbiNVy-KQGHZhypSgA2AB2QQQSsBMKL7CBmBydQhFYCdihavQokOQ4svGpxow8CA8vqvCKziD4jqVJzdEyvh-HUaD1CsWA3I8uQLgMPkYComioN4m9YFIMHaJ4PioFODD7KAA)

Because the hydration function expects SerializedResult<T, E | D> but through the early returns rather receives SerializedResult<T, never> | SerializedResult<never, E> | SerializedResult<never, D>

fiery rose
#

interesting

#

so basically, it's inferring

type LoaderReturnInferred = Promise<
    SerializedError<{code: number; msg: string;}> 
    | SerializedError<{code: number; validationErrors: string[];}> 
    | SerializedSuccess<{somedata: boolean}>
>;
#

i.e. 2 different errors or a success

#

but if it inferred this

type LoaderReturnGood = Promise<
    SerializedError<{code: number; msg: string;} | {code: number; validationErrors: string[];}> 
    | SerializedSuccess<{somedata: boolean}>
>;

it would work

#

the problem is getting there 😐

fiery rose
#

I have it mostly working

#

https://www.typescriptlang.org/play/?#code/MYewdgzgLgBASgUQMoFUAyAVA+hgmgBWRgF4YBvAKBmplQGE7kkAuAIggFdhgBTCCVgBoqNBHDgB5OGx4AnWSFmsKAXxgBDCDFCQoAbgoUoATwAOPWnICW6gDZWAXjwAmSLr34AeDAD4S5EWpndSh1ZgwDGhgTc2YYnhAAM3hkdGw8QiQAbVZ6RiQkVgBdAxVDeMtZG3snZwR5RW8-UkoouQVZcMiaeLizBOTEVEwcAmQcsUk4YtLDYFtNLQkAayaAqIA3Ow4eZhgMQJgrCBWACgBKPahZHfWomlkeKA5ZMGibnkOyqJ1oG+AoIpTltbDs9hhznd7tEABbHAB0INupCRnyiZTKFHmixg9VkngQflaNHaij2CEOxzOlxgiTsEAsxOhj2er1p9J43Wo3xov2uXEBslOpM6uMhTPuUDhEHhIv8Iq+qkMRn6lWqjhccD4HFsUE8ABFBIT-EhrHYNa53HwIAafAAfU1Vc21PGNQkGCg8AAepkUsESHDAAKs4BgDKdNR4bm41qap2CoXBNMd6tqWs4uu8ghgYB4GzkRJELJebzIMAT6mzvRSw3SYyQ8LyTBgKlmKvMMAAQpoeK7ZP4JaBnLswBwALYAIzkbe9vtk-sDwdD4dTvYa+IwMG9UB4YGcWm7DL7PmF66TexTzs12szufzsmzvjuxbZZZFVf6eyGaVGmXhkykFs2wAemAhAxx1EILAAd1kdRTFMKwwAAc1hCxHgzWAkLDEAxyeOEUM9H0-VpRcoBDN4YWMZw4J3dMdT1DBs0JU4kNMDgoAvM1I2cejMyY3EfHFSlklYsB2KgeEKmIGSax-DJkEbFAGCYYToWoF83lzaCYDONiOPhCtzkVKJNJzHgdLxMSJNlddjMMTFQMSKxdTkNCt3XLR1D3MMrX4PgYBADj2wsPsbSfUhN23Xd9zVK86nXTwkMSNzjQAflxGA9jvacQtoPyGXC5p9i3L0dz3LRLx46MPBtZK3P1PwMv1LLzPvAxgIAKk6mBHlASckIsKV0JvKAIGzCAQBgKgeoAAyqi0+08ABBe0FpdRLOzW7iLRq2M6G2iNdoK21ZpmmAp36vhzvmnaNo6Fa7S2h07pcPavDoO1GrOmBOuAihQIqOhcInQbeNGtZSHWlwlrCppDtXS0Yy8d7Cvhj1QNAHVnA0WxJoumCqigcqLuMI4wHsMAkNQ4aYFmsLZo0Hz5oKvhGZhHhbHMWQtHsZYLEm5g8uBgbc3BzDIemqJoYSh6orKmLKte2XGnq-t0sy7K8wLQ47Ti6qTvl8rYpl1GkrAFL+0amBmtanLZB8D0KgAMSsL00BAdRh1kPi9TQUrja0U54RD9RZGQiBmG84xzmIHxo+KkXQbF32Vug9QrB3ZxPC1VkwAwfpPDQHwS49PkYFsT3vf8TRjCDGALjjiUrFEgBZEIYXhOC91wi4YE8GAAAZ4QAVmM5knhLMNXr7U4yCHXYAEYACYAGYABZBDHCBkLYSa8I8jpWBUc4uSONuO677znF7yEB+HsfDgeSe2RXK9Z-nkBh2YdeR4ANgAOyCBBFYBMFEwrMByOoIQrAJzFBPnoKIhxDhmTfpGVGc994uBCGEfkPAEFKixOAaAvUAqkHUOnTOFcq5yAuAYcuVEaJQXFgxfwjDaI8F9qcDCGgtCu3djQn2EN4hJGoV7Asp9DCgWkTI2RcxiGwEruI2QAB1TOMJUY1wgHXYADdY4+AlKg16GCyAgGWHED4BCyjl1kMvGulDFGCLUVKDBkiGF2NIOwqCXDbG8JgPwj2yjU4iOSEo72ziNEFSEnoIAA

#

there are a few downsides, which may or may not matter (and may be able to be addressed in some way)

  1. my fix casts the type, but you can probably fix this by changing the input of hydratedResult
const hydratedResult = hydrateResult(res as FixLoaderResult<typeof loader>);
#
  1. if you have a loader that never returns an error, Err will still be in the returned type
OK<{ok:booolean}> | Err<never>
stuck cobalt
# fiery rose I have it mostly working

Thank you so much, this is really great! I suppose we could enhance this by not having to utilize the loader function but just using its data like this:

//TLoader is just the return type of loader, so we can pass in the data directly
export function hydrateLoaderResult<TLoader>(data: TLoader) {
    return hydrateResult(data as CombinedResult<TLoader>)
}

We'd still cast, but I think this should be fine considering we're using the generic here, and are not asserting anything...

fiery rose
#

happy to help - there's definitely ways to improve it (as always)