This exported type validates everything perfectly, but gives wrong validation error:
export type Animal = RequireSwimOrFly & RequireColorOrSizeOrVolume & (CantHaveBothColorOrSize | CanHaveNeitherColorOrSize)
const myAnimal: Animal = {
swim: "greatly",
color: "yellow",
size: "large",
}
Gives the following error:
Property 'fly' is missing in type '{ swim: string; color: string; size: string; }' but required in type 'Required<Pick<IAnimal, "fly">>'.(2322)
But that's not the correct validation! It's acceptable to NOT have fly, because it has swim. The true error is having color and size at the same time. Any way to give more intuitive errors for future developers?
Here's a working version on TS Playground
I am using multiple types to check multiple conditions:
type RequireSwimOrFly = RequireAtLeastOne<IAnimal, 'swim' | 'fly'>
type RequireColorOrSizeOrVolume = RequireAtLeastOne<IAnimal, 'color' | 'size' | 'volume'>
type CantHaveBothColorOrSize = RequireOnlyOne<IAnimal, 'color' | 'size'>
type CanHaveNeitherColorOrSize = Omit<IAnimal, 'color' | 'size'> & { color?: never, size?: never }
With the requires defined as follow:
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys]
And the interface:
interface IAnimal {
swim?: string;
fly?: string;
color?: string;
size?: string;
volume?: string;
}
Ultimately your
Animal
type is a large union type of object types; the intersections tend to be distributed over the unions and an intersection of object types can generally be collapsed together. You can even write a helper "identity" utility type to make that happen explicitly when you view it with IntelliSense:which gives you this:
If you give the compiler a value which is not of type
Animal
, that means it is not assignable to any of the members of the union. It fails each and every one. A full reporting of why that value is inappropriate would be very, very long, as the compiler mentions each member of the union and how your value fails to match it. Something like:Some unions have thousands of members, so it is not realistic for the compiler to give a full accounting of all the ways in which a value fails to be assignable to it. It has to do something else.
Now, it could try to find "the closest" member of the union to the value according to some metric, and then report only an error there. Or maybe it could try to find "the most common" reason for failure and report that. Or maybe it could do some other more human-friendly heuristic. But it doesn't do any of those things; it picks the last member in the union, and reports that one:
This message is not wrong. It's just not particularly illuminating or intuitive.
There was a GitHub issue filed at microsoft/TypeScript#4451 asking for something better, but it was closed as "Won't Fix", with the main explanatory comment being that it wouldn't be obvious how to write a good algorithm here.
So that's, unfortunately, how it is. You get some error message that correctly explains part of the reason why your value is bad, but not necessarily "the" reason a clever human being might give.
Playground link to code