Revanth M
Revanth M

Reputation: 113

Unable to get nested type guards to work with union types in typescript

I am trying to clean the data by removing any falsy/error values before passing it to my consuming function. But I am not able to get the type guards working correctly.

I am running into the following errors

Type 'ExcludeUnusableValues' is not assignable to type 'T'.

and

Type 'Error | { prop1: string; prop2: string; }' is not assignable to type '{ prop1: string; prop2: string; }'.

interface BaseVariant {
  id: number
  data: {[s:string]:any} | Error
}

interface VariationOne extends BaseVariant {
  type: 'One'
  key1: string
  data:
  | {
      prop1: string
      prop2: string
    }
  | Error
}
interface VariationTwo extends BaseVariant {
  type: 'Two'
  key2: number
}
interface VariationThree extends BaseVariant {
  type: 'Three'
  key3: number
  data:
  | {
      prop3: string
      prop4: number
    }
  | Error
}

type Variations = VariationOne | VariationTwo | VariationThree

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>

// Removes falsy/error values from object properties
type ExcludeUnusableValues<
  T,
  K extends keyof T,
  C = Error | null
> = Omit<T, K> & { [a in K]-?: Exclude<T[a], C> }

const hasValidData = <T extends Variations>(item: T): item is ExcludeUnusableValues<T, 'data'> => {
  return !(item.data instanceof Error)
} 

const foo = (item: Variations) => {
  if (!item || !hasValidData(item)) {
    return null
  }

  switch (item.type) {
    case 'One':
      return barOne(item);
    case 'Two':
      return barTwo(item);
    case 'Three':
      return barThree(item);

  }
}

const barOne = (item: ExcludeUnusableValues<VariationOne, 'data'>) => {
  console.log(item.data.prop1)
}
const barTwo = (item: ExcludeUnusableValues<VariationTwo, 'data'>) => {
  console.log(item.data)
}
const barThree = (item: ExcludeUnusableValues<VariationThree, 'data'>) => {
  console.log(item.data.prop3)
}

TypeScript PlayGround Link

Upvotes: 1

Views: 1048

Answers (1)

zhirzh
zhirzh

Reputation: 3449

I'll try to answer it by first cleaning up the snippet since there's a lot going on:

// helper types
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Truthy<T, F> = { [K in keyof T]-?: Exclude<T[K], F> }
type PickTruthy<T, K extends keyof T, F = Error | null> = Pick<Truthy<T, F>, K>


// main code
interface VariationOne {
  type: "One"
  data: { foo: string } | Error
}

interface VariationTwo {
  type: "Two"
  data: { baz: number } | Error
}

type Variations = VariationOne | VariationTwo


declare function hasValidData<T extends Variations>(item: T): item is PickTruthy<T, "data">

Now we see the real error in hasValidData() (see in playground):

A type predicate's type must be assignable to its parameter's type.

Type Pick<Truthy<T, Error>, "data"> is not assignable to type T.

This happens because type predicates only refine (narrow down) one type into another. If the result type is "wider" than the target type, it won't work. We can confirm this with the help of unknown (widest/Top type) and never (narrowest/Bottom type) (see in playground).

type Foo = number

declare function UnknownisFoo(x: unknown): x is Foo
declare function FooisNever(x: Foo): x is never

declare function NeverisFoo(x: never): x is Foo // ERROR!
declare function FooisUnknown(x: Foo): x is unknown // ERROR!

This means that in my modified version of your original code:

  1. Type PickTruthy<T, "data"> is also narrower than Variations: An object with more keys is more "specific"
  2. Type parameter T extends Variations is narrower than Variations: T is a SubType of Variations

Since there's no direct comparison between the two types, you can't refine the target type parameter the way you described.

Your best bet is to check whether item.data itself is an error or not using an isError() predicate guard

interface VariationOne {
  type: "One"
  data: { foo: string } | Error
}

interface VariationTwo {
  type: "Two"
  data: { baz: number } | Error
}

type Variations = VariationOne | VariationTwo


declare const x: Variations
const { data } = x

declare function isError(item: unknown): item is Error

if (isError(data)) {
  console.log('ERROR:', data.message)
}

Upvotes: 1

Related Questions