Home > front end >  Typescript Unions - I do not understand
Typescript Unions - I do not understand

Time:10-24

I have experimented with creating custom types recently and doing so, I came across an error which I really do not understand. Assume the following setup:

Type Definitions

type Error = {
  error: string
}

type Item = {
  id: string
  name: string
}

type Result = Item | Error

Test Function

const testFunc = ( item: boolean ): Result => {
  return item ? { id: '0x1', name: 'My Item' } : { error: 'An error has occurred }; 
}

Progam

const item = testFunc(true);
if ( item.error ) {          // <-- TS complains that `error` does not exist on type `Result`
  console.log('error');
}
console.log('Happy days!', item)

TS keeps complaining that

Property 'error' does not exist on type 'Response'.

Property 'error' does not exist on type 'Item'.ts(2339)

If I remove Item from Result it obviously works. Do I have something misconfigured or do I get this completely wrong? I have basically read through here and thought that this should be a very easy task.

Thanks! Any help appreciated!

CodePudding user response:

When you get a type that has union of different types and you are trying to access it - the type system needs to know which type of "this multi-types / unions do you mean?" because it cannot know what are trying to reference.

You can one of this solutions below:

  • TypeCasting the variables when you want to access it as error if ((item as Error).error) {}

  • TypeCasting the return const item = testFunc(true) as Error

CodePudding user response:

By specifying that the return type of the function is Result, the returned item is inferred to be (due to how you've specified the type):

{ error: string }
| { id: string, name: string }

TypeScript doesn't go into the local paths of the function and narrow down the type of the possible return value, not without overloading or generics or something.

You can fix this by removing your annotated type-widening so that TS can infer that the return value is of type:

{
    id: string;
    name: string;
    error?: undefined;
} | {
    error: string;
    id?: undefined;
    name?: undefined;
}

Which is easier to work with than your original types, because possibly having properties whose values are undefined means that those properties are easily accessible. (In contrast, something like { id: string; name: string } does not have the error property at all, so error can't be accessed on it without narrowing down the type first, like with in)

I'd also recommend not using type Error - don't shadow the global Error, call it something else.

const testFunc = (item: boolean) => {
    return item ? { id: '0x1', name: 'My Item' } : {
        error: 'An error has occurred'
    };
}

will compile successfully.

CodePudding user response:

You need a type guard before accessing a property or a method that is not common to all the types in your Union Type.

In this case, the easiest would probably to check whether your item has a property named error:

const item = testFunc(true);
if ( 'error' in item ) {
  console.log(item.error);
} else {
  console.log('Happy days!', item)
}

If you were using classes instead of types, you could also use if(item instanceof MyError) instead of if ( 'error' in item ).

If you types were primitive (boolean, string, number), you could also use if(typeof item === 'string') instead of if ( 'error' in item ).

BTW, as @CertainPerformance mentioned, don't use Error.

  • Related