Vincent
Vincent

Reputation: 5445

How can I do a runtime check of both legs of a union type?

I have a variable with the following type:

const data: { error: string; } | { property1: string; };

(It's the response of an external API, so I can't change it.)

I now want to check which of the two possible types data is. I tried this:

if (typeof data.error !== 'undefined') { }

Unfortunately, TypeScript then complains that

Property `error` does not exist on type `{ property1: string }`.

How do I get TypeScript to know which of the possible types it is?

Upvotes: 1

Views: 48

Answers (3)

jcalz
jcalz

Reputation: 330501

This is precisely why TypeScript has user-defined type guards. You can tell TypeScript how to distinguish between the two consitutents of the union:

function dataHasError(data: { error: string; } | { property1: string; }): data is { error: string; } {
  return 'error' in data;
}

And then it will narrow types the way you expect inside if/else clauses:

const data: { error: string; } | { property1: string; } = /* API response */;

if (dataHasError(data)) {
  console.log(data.error); // okay
} else {
  console.log(data.property1); // okay
}

If you often find yourself needing to do this type narrowing by checking for the presence of a key, you can put the following type guard in your library:

function hasKey<K extends string>(obj: any, key: K): obj is Record<K, any> {
  return key in obj;
}

Then the above can be written like this:

if (hasKey(data, 'error')) {
  console.log(data.error); // okay
} else {
  console.log(data.property1); // okay
}

Relevant note: there is a suggestion for TypeScript to automatically interpret key in obj as a type guard, so that the following would work with no errors:

if ('error' in data)) {
  console.log(data.error); // would be okay, currently error
} else {
  console.log(data.property1); // would be okay, currently error
}

Currently (as of TypeScript v2.5) that is not part of the language, so a user-defined type guard is the best answer for now.


Hope that helps; good luck!

Upvotes: 3

Vincent
Vincent

Reputation: 5445

This is the workaround I'm currently using. As a workaround, it's fine, but I'd rather use TypeScript's type system properly:

const data: any = /* API response */;

if (data.error) {
  const errorData: { error: string; } = data;
  // Do whatever
}

const actualData: { property1: string; } = data;

Upvotes: 0

kamyl
kamyl

Reputation: 6396

One simple way I can think of is to use one type with optional properties instead of union types. Like this:

interface ApiResponse = {
    error?: string;
    property1?: string;
}

const errorData: ApiResponse = { error: 'error data' };
const okData: ApiResponse = { property1: 'ok data' };

console.log(typeof errorData.error !== 'undefined'); // true
console.log(typeof okData.error !== 'undefined'); // false

// or even cleaner:

console.log(errorData.error); // truthy
console.log(okData.error); // falsey

console.log(!!errorData.error); // true
console.log(!!okData.error); // false

Upvotes: 0

Related Questions