Recently I've discovered some interesting information about the empty object type, {}
, and some weird features and tricks that it provides.
It's not "just an empty object literal" as some might think, and there are several very interesting and useful ways of utilizing its unique properties, that you could use in many different scenarios.
& {}
is really weird
First of all, it's a common misconception that {}
denotes an empty object. It's actually "any value that is defined" or, in other words, a non-nullish value. You can check this by intersecting a nullable type with {}
:
type MaybeString = string | null | undefined;
type Name = MaybeString & {};
type Name = string
// Also, ^take a look^ at these annotations/popups that I've implemented on the site!
That's actually how the internal NonNullable<T>
utility type is implemented, and that's exactly how I found out about this trick!
type NonNullable<T> = T & {};
But that's not everything that & {}
has to offer...
Auto-completion for literals
Let's say you've got a type like this, with some useful pre-defined literals:
type Color = "red" | "orange" | "yellow";
And, sometimes, you need to specify other values, aside from those specified, for example "blue"
or "#54bba2"
. You'd think that putting string
in the union would work...
type Color = "red" | "orange" | "yellow" | string;
type Color = string
But nope! The string
type simply absorbs the rest of the literals. "red" | "orange" | "yellow"
extends string
, after all. But there is a weird trick that you can use here, which I found while browsing HTML DOM type declarations, — use (string & {})
instead.
type Color = "red" | "orange" | "yellow" | (string & {});
type Color = (string & {}) | "red" | "orange" | "yellow"
Now, string
is constrained by {}
and can't absorb the literals. This will allow your IDE to provide auto-completion of the specified literals, while still allowing you to use any other strings. (I'll explain how this works later in the post)
const red: Color = ""
▤ "orange"
▤ "red"
▤ "yellow"
const teal: Color = "#54bba2";
Short-circuiting with strings
Sometimes I'd short-circuit expressions with strings and encounter issues like this:
function getString(): string | undefined;
function getNumber(): number;
const value = getString() && getNumber();
const value: number | "" | undefined
TypeScript does its job, and determines that it's possible for this value to have a value of ""
, since it's considered falsy and it wouldn't evaluate getNumber()
. But the thing is, in my case, getString()
always returns a truthy string, so, it just complicates things.
Well... Believe it or not, (string & {})
fixes things here as well!
function getString(): (string & {}) | undefined;
function getNumber(): number;
const value = getString() && getNumber();
const value: number | undefined
So, apparently, {}
excludes ""
from string
? And yet, the code below still works:
const text: {} = "";
// No errors here!
const text2: string & {} = "";
// None here either!
If we were to turn the situation around, and short-circuit on numbers instead, we'd get into a similar-looking scenario:
function getNumber(): number | undefined;
function getString(): "text";
const value = getNumber() && getString();
const value: 0 | "text" | undefined
function getNumber(): (number & {}) | undefined;
function getString(): "text";
const value = getNumber() && getString();
const value: "text" | undefined
Dissecting & {}
So... Intersecting types with {}
removes falsy values such as null
, undefined
, ""
and 0
?
Kind of, but not really... Take a look at this:
type Test_A = (string | null | undefined) & {};
type Test_A = string
type Test_B = ("" | "text") & {};
type Test_B = "" | "text"
type Test_C = (0 | 123) & {};
type Test_C = 0 | 123
type Test_D = (string | false) & {};
type Test_D = string | false
& {}
only works like this when intersected with string
or number
directly.
type Test_A = string & {};
type Test_A = string & {}
type Test_B = number & {};
type Test_B = number & {}
I looked into it and found a pull request that implemented this behavior with {}
. Turns out, string
, number
and bigint
are special exceptions to the type reduction rules:
For backwards compatibility, special exceptions to the
T & {}
type reduction rules existing for intersections written explicitly asstring & {}
,number & {}
, andbigint & {}
(as opposed to created through instantiation of a generic typeT & {}
). These types are used in a few frameworks (e.g.react
andcsstype
) to construct types that permit anystring
,number
, orbigint
, but has statement completion hints for common literal values. For example:type Alignment = string & {} | "left" | "center" | "right";
The special
string & {}
type prevents subtype reduction from taking place in the union type, thus preserving the literal types, but otherwise anystring
value is assignable to the type.
This inconsistent behavior allows for some cracks in TypeScript's type safety:
type TruthyString = string & {};
const truthy: TruthyString = "";
const id = truthy && 123;
const id: 123
// No errors!
const id2 = "" && 123;
const id2: ""
// This kind of expression is always falsy. ts(2873)
const id3 = ("" as string) && 123;
const id3: "" | 123
// This kind of expression is always falsy. ts(2873)
Conclusion
This behaviour is pretty obscure, but it could still pose some risks to various projects, if a developer finds this trick and doesn't fully understand its scope. Don't misunderstand, it's great that TypeScript provides a solution for several of the problems that I've encountered: auto-completion of literals and short-circuiting with always-truthy values...
You just need to be careful, and be aware that ""
and 0
could slip through if you use & {}
. One of the ways to guard against that is to use equality operators ==
, !=
, ===
, !==
, although, I personally find it hard to properly use them, since it bloats the JS bundle ever so slightly... I like to think that I always examine the code carefully, and account for all edge cases, but a falsy value will probably eventually slip through and break something...
Don't be like me, — do proper equality comparisons!