Lawrence Wagerfield
Lawrence Wagerfield

Reputation: 6611

TypeScript: Type to contain any value EXCEPT values from a predefined set

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

Answers (2)

jcalz
jcalz

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!

Playground link to code

Upvotes: 4

Steven Spungin
Steven Spungin

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

Related Questions