Reputation: 113
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)
}
Upvotes: 1
Views: 1048
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 typeT
.
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:
PickTruthy<T, "data">
is also narrower than Variations
: An object with more keys is more "specific"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