Is there a way to write a typeguard against a complex combinatory type that preserves previous type information?

33 views Asked by At

Is there a way to write a TypeScript typeguard against a complex combinatory type (that uses many "and" and "or" statements) to test the existence of one of the "or" types?

For example:

interface Type1 { cat: string }
interface Type2 { dog: boolean }
interface Type3 { mouse: number }
interface Type4 { elephant: string | number }
type CombinatoryType = (Type1 & (Type2 | Type3)) | Type4;

If we have a typeguard to check the existence of Type2 and Type4 (called hasType2 and hasType4 respectively), calling them in the following order should produce the following results:

function doSomething(data: CombinatoryType) {
    if (hasType2(data)) {
        // 'data' is now of type: (Type1 & Type2) | Type4

        if (hasType4(data)) {
            // 'data' is now of type: Type1 & Type2 & Type4
       }
    }
}

And calling them in the opposite order should produce the following results:

function doSomething(data: CombinatoryType) {
    if (hasType4(data)) {
        // 'data' is now of type: (Type1 & (Type2 | Type3)) & Type4

        if (hasType2(data)) {
            // 'data' is now of type: Type1 & Type2 & Type4
       }
    }
}

Important to note:

  • As demonstrated above, the typeguard should not lose previous type information (e.g. from previous type assertions made)
  • The typeguard should also not use an explicit return type (e.g. data is (Type1 & Type2) | Type4) as the combinatory type could get very long. This also tricks VSCode's type inference into thinking the asserted value is exactly that type (although intellisense still works fine).
1

There are 1 answers

3
Keyboard Corporation On

If my understanding is correct in your provide code, you want to create type guards for these Type2 and Type4 in the CombinatoryType type. Isn't it?

If so, here is an example I can provide:

function hasType2(data: CombinatoryType): data is (Type1 & Type2) | Type4 {
   return 'dog' in data;
}

function hasType4(data: CombinatoryType): data is (Type1 & (Type2 | Type3)) & Type4 {
   return 'elephant' in data;
}

In the above sample, we're using the in operator to check if the dog property exists in data for hasType2, and if the elephant property exists in data for hasType4.

The in operator is a type guard that checks if a property exists in an object.

The data is (Type1 & Type2) | Type4 and data is (Type1 & (Type2 | Type3)) & Type4 syntax is used to tell TypeScript that if the function returns true, then data is of the specified type.

Now, you can use these type guards in your doSomething function:

function doSomething(data: CombinatoryType) {
   if (hasType2(data)) {
       // 'data' is now of type: (Type1 & Type2) | Type4

       if (hasType4(data)) {
           // 'data' is now of type: Type1 & Type2 & Type4
       }
   }
}

In this function, if hasType2(data) returns true, then that data is of type (Type1 & Type2) | Type4 within the first if. If hasType4(data) also returns true , then that data is of type Type1 & Type2 & Type4 within the second if.

The same logic applies to the second doSomething function, where the type guards are called in the opposite order.

Edit

I will provide this sample;

type Type1 = { /* ... */ };
type Type2 = { /* ... */ };
type Type3 = { /* ... */ };
type Type4 = { /* ... */ };

type CombinatoryType = Type1 & Type2 & Type3;

function hasType2(data: CombinatoryType): data is CombinatoryType & Type2 {
 // Check if 'data' has properties of Type2
 return (data as Type2) /* ... */ !== undefined;
}

function hasType4(data: CombinatoryType & Type2): data is CombinatoryType & Type2 & Type4 {
 // Check if 'data' has properties of Type4
 return (data as Type4) /* ... */ !== undefined;
}

function doSomething(data: CombinatoryType) {
  if (hasType2(data)) {
      // 'data' is now of type: (Type1 & Type2) | Type4

      if (hasType4(data)) {
          // 'data' is now of type: Type1 & Type2 & Type4
      }
  }
}

In the sample above, hasType2 and hasType4 functions, they check whether data has properties of Type2 and Type4 respectively, and if so, they narrow down the type of data to CombinatoryType & Type2 and CombinatoryType & Type2 & Type4 respectively. This allows you to preserve the previous type information of data and add new types to it.