At Artsy, we love TypeScript. We use it in most of our node/web/mobile repos. Today, I want to talk about a specific case we found while trying to make our types more strict on palette-mobile, which is our Design System for React Native.
Check this out:
const welp: "hello" | "world" | string // `welp` is of type `string`.
Like the comment says, even though we have two specific strings, the fact that
we do a union with string
, makes welp
have a type of just string
. This is
because both "hello"
and "world"
are strings, and the union tends to go to
the type that includes the most.
Think of set theory and bubbles.
"hello"
is a type by itself, and "world"
is a type by itself. Unioning them
together gives us a new type, which is a bubble that contains both "hello"
and
"world"
. In that "hello" | "world"
union bubble, we see both "hello"
and
"world"
types as subsets.
The string
bubble contains all strings, so it contains "hello"
and "world"
and "hello" | "world"
, so the union of them with string is string.
That is usually ok, but for our case, it didn’t work. Here is what we wanted to do.
The problem
In our Design System, we have certain color, named like black100
, black80
,
blue100
, red150
etc. We can have a type like
type ColorDSValue = "black100" | "black80" | "blue100" | "red150" // | etc
and that works great. We get to have autocomplete, typechecking, all the good stuff that TypeScript brings.
But we also want to support any other string, like "#000000"
, "#000"
,
"rgb(0,0,0)"
, "rgba(0,0,0,0.5)"
, "hsl(0,0%,0%)"
, "hsla(0,0%,0%,0.5)"
.
Ok, you might say, just make more types like
type ColorHexValue = `#${string}`
type ColorRGBValue = `rgb(${number},${number},${number})`
type ColorRGBAValue = `rgba(${number},${number},${number},${number})`
type ColorHSLValue = `hsl(${number},${number}%,${number}%)`
and so on. That’s great. So far, so good.
We also want to make sure CSS color names are accepted. So then we add something like
type ColorCSSString = "red" | "blue" | "hotpink" // | etc
and now we have a type with all the values. That seemed ok, but it also felt a bit too much. If CSS names change, we need to update. Also what we wanted to do is actually have autocomplete and typechecking for our DS values, and just leave it loose for all the rest.
So we tried
type ColorDSValue = "black100" | "black80" | "blue100" | "red150" // | etc
type ColorOtherString = string
type Color = ColorDSValue | ColorOtherString
but we ended up with Color
being just string
, which automatically means no
autocomplete and no typechecking.
Now check this out!
const wow: ("hello" | "world") | (string & {}) // `wow` is of type `"hello"` or `"world"` or `string`.
This weird-looking intersection of string & {}
makes it so that the specific
strings "hello"
and "world"
are distinguished from string
as a whole type.
The way this works is this:
- the intersection of
string
and{}
(which isstring & {}
), is essentially the same asstring
, but it is a new type, different fromstring
. - the union of
"hello"
and"world"
is"hello" | "world"
, which is a new type, different from"hello"
and"world"
. It contains both. - the union of
"hello" | "world"
andstring
expands the type tostring
, since that is the common type."hello"
,"world"
, and"hello" | "world"
, all inherit fromstring
. - the union of
"hello" | "world"
andstring & {}
is"hello" | "world" | (string & {})
, which is a new type, different from juststring
. This is because"hello"
and"world"
DO NOT inherit fromstring & {}
, so they are distinguished fromstring & {}
as a whole type.
With this type trick, essentially we can tell the type system that we want specific string, but also any other string.
Here is a complete view of the sets.
It seems pretty funky that string
and string & {}
are same in a way, but
different in another way. They both tell the type system that any string is
accepted. But one is inherited by every type that is a string (like
type Hi = "hello"
), where as the other is not inherited, so they are
distinguished from each other.
That is so cool to me! I wanted to do this and didn’t even have the words to describe it, I didn’t know how to google it. We kind of found it accidentally.
This is so so useful for types or props where you want the general type for
support (string
), but you also want the specific type for autocomplete
("black100"
). It made my whole week when I figured that out and made that
color type.
Here is the final type:
type ColorDSValue = "black100" | "black80" | "blue100" | "red150" // | etc
type ColorOtherString = string & {}
type Color = ColorDSValue | ColorOtherString
Now we have autocomplete and typechecking.
Final thoughts
This is such a useful little TypeScript trick. Thanks to Sultan for finding this. He found it in a TypeScript issue. Then we tried it and figured out how to work with this, and how to make our type exactly what we wanted, for the best DX we can get.
Link to palette-mobile, where we use this type: link
Link to a TypeScript playground with the examples: link