ZiiMakc
ZiiMakc

Reputation: 36836

Typescript one type or another only

I want to return an object as fetch response that can have one of the property (data or mes):

{ data: Data } | { mes: ErrMessage }

Problem is that typescript complains about this object, let's say props:

if (prop.mes) return // Property 'mes' does not exist on type '{ data: string; }'.
prop.data // Property 'data' does not exist on type '{ mes: string; }'.

Is there any alternative except (it's not convenient to define this big type in component every time):

{ data: Data, mes: false } | { mes: ErrMessage, data: false}

Typescript playground example.

type ErrMessage = string // some complex structure

const fetchAPI = <Body = any, Data = any>(link: string, body: Body): Promise<{ data: Data } | { mes: ErrMessage }> => {
  return new Promise((resolve) => {
      fetch(link, { body: JSON.stringify(body) })
      .then((raw) => raw.json())
      .then((res) => {
        resolve(res.err ? { mes: res.err } : { data: res })
      })
      .catch((err) => {
        resolve({ mes: 'text' })
      })
  })
}

type Data = string // some complex structure
type MyReq = string // some complex structure
type MyRes = {data: Data } | {mes: ErrMessage}

const someLoader = async () => {
    const res = await fetchAPI<MyReq, MyRes>('reader', 'body')
    return {res}
}

const componentThatGetProp = (prop: MyRes ) => {
    // error handler
    if (prop.mes) return // Property 'mes' does not exist on type '{ data: string; }'.

    // do something
    prop.data // Property 'data' does not exist on type '{ mes: string; }'. 
}

Upvotes: 1

Views: 1323

Answers (2)

jcalz
jcalz

Reputation: 328097

The "right" type for TypeScript to use is

type MyRes = { data: Data, mes?: undefined} | { mes: ErrMessage, data?: undefined}

where the irrelevant properties in each member of the union are optional and have an undefined value, as opposed to a false value. (Or you could use never instead of undefined, since optional properties always get | undefined added to them, and never | undefined is just undefined.) That corresponds to what will actually happen if you check an irrelevant property: it will be missing. As of TypeScript 3.2, the compiler treat the above type as a discriminated union and your errors will go away:

const componentThatGetProp = (prop: MyRes) => {
  // error handler
  if (prop.mes) return // okay
  // do something
  prop.data // okay
}

I agree that it would be a pain to manually turn a union type your existing MyRes into one where each union member contains all the keys from all the other union members. Luckily you don't need to do this manually. TypeScript's conditional types let you programmatically manipulate a "regular" union into the discriminated form above. Here's one way to do it, using a type alias called I'll call ExclusifyUnion<T> to indicate that it takes a union type T and turns it into a new union type where the compiler can tell that any value of that type must match one member of the union exclusively:

type AllKeys<T> = T extends any ? keyof T : never;

type ExclusifyUnion<T, K extends AllKeys<T> = AllKeys<T>> =
  T extends any ?
  (T & { [P in Exclude<K, keyof T>]?: never }) extends infer U ?
  { [Q in keyof U]: U[Q] } :
  never : never;

I can explain that if you want, but the sketch is this: AllKeys<T> returns all keys from a union T, so AllKeys<{a: string} | {b: number}> is "a" | "b". And ExclusifyUnion<T> takes each member of the union and intersects it with a type containing all the rest of the keys as optional properties of type never. So ExclusifyUnion<{a: string} | {b: number} | {c: boolean}> will end up looking like {a: string, b?: never, c?: never} | {b: number, a?: never, c?: never} | {c: boolean, a?: never, b?: never}. Here's what it does to MyRes:

type MyRes = ExclusifyUnion<{ data: Data } | { mes: ErrMessage }>
/*type MyRes = {
    data: string;
    mes?: undefined;
} | {
    mes: string;
    data?: undefined;
}*/

And this should scale up easily even if MyRes has more union members with more properties.


Okay, hope that gives you a path forward. Good luck!

Playground link to code

Upvotes: 2

phry
phry

Reputation: 44086

Instead of

if (prop.mes) return

just try

if ('mes' in prop) return

one tries to access the property (which might not be there), the other checks for it's existence

Upvotes: 5

Related Questions