Home > OS >  How to use negated types in TypeScript?
How to use negated types in TypeScript?

Time:10-31

How can I use a negated or a "not" type in Typescript?

example(just an example), I tried to use is not in a type predicate:

function notUndef(obj: any): obj is not undefined {
// -------------------------------> ~~~ error!
// cannot find name 'not'.
    return obj !== void(0);
}

and

function notUndef(obj: any): (typeof obj !== undefined) {
//                   ')' expected -----> ~~~   -----> ~
//                                      ';' expected
    return obj !== void(0);
} 

but I received errors.

Neither of these work, what can I do?

CodePudding user response:

You may be looking for combining generics with the type predicate:

function notUndef<T>(obj: T): obj is Exclude<T, undefined> {
    return obj !== void(0);
}

CodePudding user response:

TypeScript does not directly support negated types, so there's no general way to express "the complement of T". There was an implementation of negated types at microsoft/TypeScript#29317 using the not keyword exactly as you are using it in your example. But this feature was never merged into the language.

There are workarounds which may or may not fit your use case. In the particular case of not undefined, you can write the equivalent type {} | null. The empty object-like type {} accepts every value except null or undefined (see this answer for more info), so the union of {} with null results in a type that accepts every value except undefined:

function notUndef(obj: any): obj is {} | null {
    return obj !== void (0);
}

const x = Math.random() < 0.5 ? "abc" : undefined;
if (notUndef(x)) {
    // x: string
    console.log(x.toUpperCase()); // "ABC" if reached
}

const y = ["abc", 123, { a: 456 }, null, undefined][
    Math.floor(Math.random() * 5)];
// const y: string | null | {a: number} | null | undefined
if (notUndef(y)) {
    y // y: string | null | {a: number} | null
} else {
    y // y: undefined
}

In general, if the type you'd like to negate can be filtered out of a union type, then you can use a generic type parameter and the Exclude<T, U> utility type to filter the to-be-negated type out of whatever union is passed in. Here's a way to "negate" an existing user-defined type guard function (as long as filtering a union works with it):

function not<U>(guard: (obj: any) => obj is U) {
    return <T,>(obj: T): obj is Exclude<T, U> => !guard(obj);
}

Let's test it with undefined:

function isUndefined(obj: any): obj is undefined {
    return obj === void (0);
}
const notUndef = not(isUndefined);
// const notUndef: <T>(obj: T) => obj is Exclude<T, undefined>
const x = Math.random() < 0.5 ? "abc" : undefined;
if (notUndef(x)) {
    // x: string
    x.toUpperCase();
}

And how about with number:

function isNumber(obj: any): obj is number {
    return typeof obj === "number";
}
const notNumber = not(isNumber);
// const notNumber: <T>(obj: T) => obj is Exclude<T, number>

const y = Math.random() < 0.5 ? "abc" : 123;
if (notNumber(y)) {
    // y: "abc"
    y.toUpperCase();
} else {
    y.toFixed();
    // y: 123
}

So that works.


There are limits to these approaches, though. Some types that cannot be negated trivially. Imagine you had

interface Foo { a: number, b: string }

and you wanted to write not Foo. You could manually build up a type like:

interface Foo { a: number, b: string }
type AllTypes = string | boolean | bigint | null | undefined | symbol | object | number;
type NotFoo =
    Exclude<AllTypes, object> |
    { a?: Exclude<AllTypes, number>, [k: string]: any } |
    { b?: Exclude<AllTypes, string>, [j: string]: any }

And I think it works:

let nf: NotFoo;
nf = 3;
nf = {};
nf = { a: 2, b: 3, c: 10 } // okay
nf = { a: "", b: "", c: 10 } // okay
nf = { a: 2, b: "", c: 10 } // error

but it doesn't scale well. You could use the generic Exclude version, which works well when you pass in a union of known things:

function isFoo(obj: any): obj is Foo {
    return obj && typeof obj === "object" &&
        "a" in obj && typeof obj.a === "number" &&
        "b" in obj && typeof obj.b === "string"
}
const notFoo = not(isFoo);
// const notFoo: <T>(obj: T) => obj is Exclude<T, Foo>

const y = [3, {}, {a: 2, b: 3, c: 10}, {a: "", b: "", c: 10}, {a: 2, b: "", c: 10}][
    Math.floor(Math.random() * 5)];
if (notFoo(y)) {
    y 
    // const y: number | 
    // { a?: undefined; b?: undefined; c?: undefined; } | 
    // { a: number; b: number; c: number; } | 
    // { a: string; b: string; c: number; }
} else {
    y // const y: {  a: number; b: string; c: number; }
}

but fails when you are passing in a type where you can't just filter out some of the members, such as the unknown type:

const z: unknown = "           
  • Related