Reputation: 6611
Is it possible to have a type that contains any value BUT the values from a predefined set?
type Fruit = 'Apple' | 'Banana' | 'Orange'
type NotFruit = ???
const a: NotFruit = 'Carrot'; // Compiler OK.
const b: NotFruit = 'Apple'; // Compiler ERROR.
i.e. does there exist a definition for NotFruit
such that the compiler responds as per the comments in my code?
Upvotes: 4
Views: 220
Reputation: 328292
Negated types as described in microsoft/TypeScript#29317 are not currently supported in TypeScript as specific types. There is no way to directly say string & not Fruit
.
For now, the only way to express negated types is indirectly, via a generic type that verifies whether a condition is met.
Something like:
type DefinitelyNot<T, C> = [T] extends [C]
? Invalid<[C, "is prohibited because it might be", T]>
: [C] extends [T]
? Invalid<[C, "is prohibited because it is assignable to", T]>
: C;
The type DefinitelyNot<T, C>
takes a type T
to negate, and a candidate type C
. If we can be sure that C
is not compatible with T
, then we return C
itself. Otherwise, we return something that C
will not match, specifically an Invalid
type that causes an error. Well, we would do that if invalid types as described in microsoft/TypeScript#23689 we supported, which they're currently not. So we need a workaround there too:
type Invalid<Msg> = Msg & Error;
It makes for some fairly ugly error messages, but at least the developer might have some chance of figuring out why the error appeared. Then we can make a function which takes a type T
and produces a new function that only accepts arguments not compatible with T
:
const makeNot = <T,>() => <C,>(val: C & DefinitelyNot<T, C>): C => val;
Let's try it out:
type Fruit = "Apple" | "Banana" | "Orange";
const asNotFruit = makeNot<Fruit>();
const a = asNotFruit("Carrot"); // okay
const b = asNotFruit("Apple"); // error
// ┌--------------> ~~~~~~~
// ["Apple", "is prohibited because it is assignable to", Fruit]
function someRandomFunction(x: string, y: number) {
const c = asNotFruit(x); // error
// ┌---------------> ~
// [string, "is prohibited because it might be", Fruit]
const d = asNotFruit(y); // okay (a number cannot be Fruit)
}
As you can see, "Carrot"
and number
were accepted because those are definitely not Fruit
. "Apple"
was rejected because it definitely is Fruit
, and string
was rejected because it might be Fruit
.
Not sure if you want to use this sort of solution, but I figured I'd include it anyway. Hope that helps. Good luck!
Upvotes: 4
Reputation: 29109
I would answer that this is definitely not possible with Typescript. There is no negation operator for sets in the language.
You can create an instanceOf typeguard though.
https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
type Fruit = "Apple" | "Orange";
function isFruit(fruit: string): fruit is Fruit {
return !(['Apple', 'Orange'].includes(fruit));
}
Upvotes: 3