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 = "