# Making sense of TypeScript utilizing set principle

*by*Phil Tadros

I have been working with TypeScript for a protracted very long time. I believe I am not too unhealthy at it. Nonetheless, to my despair, some low-level behaviors nonetheless confuse me:

- Why does
`0 | 1 extends 0 ? true : false`

consider to`false`

? - I am very ashamed, however I typically confuse “subtype” and “supertype”. Which is which?
- Whereas we’re at it, what are sort “narrowing” and “widening”, and the way do they relate to sub/supertypes?
- If you would like an object that satisfies each
`{ identify: string }`

and`{ age: quantity }`

, do you`&`

or`|`

? Each make some sense, since I desire a*union*of the performance in each interfaces, however I additionally need the thing to fulfill left & (and) proper interfaces. - How is
`any`

totally different from`unknown`

? All I get is imprecise mnemonics like “Keep away from Any, Use Unknown”. Why? - What, precisely, is
`by no means`

? “A worth that*by no means*occurs” may be very dramatic, however not too exact. - Why
`no matter | by no means === no matter`

and`no matter & by no means === by no means`

? - Why on earth is
`const x: {} = true;`

legitimate TS code?`true`

is clearly not an empty object.

I used to be performing some analysis on `by no means`

, and stumbled upon Zhenghao He is Complete Guide To TypeScript’s Never Type (try his weblog, it is tremendous cool!). It mentions {that a} sort is only a set of values, and — increase — it clicked. I went again to the fundamentals, re-formulating every part I find out about TS into set-theoretic phrases. Comply with me as I:

- Refresh my information of set principle,
- Map TS ideas to their set counterparts,
- Begin easy with booelan, null and undefined varieties,
- Lengthen to strings and numbers, discovering some varieties that TS can’t categorical,
- Leap into objects, proving my assumptions about them unsuitable,
- Lastly acquire confidence writing
`extends`

caluses, - And put
`unknown`

and`any`

the place they belong.

Ultimately, I remedy most of my questions, develop a lot cozier with TS, and give you this sensible map of TS varieties:

## Set principle

First up, a refresher on set principle. Be happy to skip in the event you’re a professional, however my algebra abilities are a bit rusty, so I might use a reminder of the way it works.

Units are unordered collections of objects. In kindergarten phrases: say now we have two apples aka *objects* (let’s name them ivan and bob, lets?), and a few baggage aka *units* the place we are able to put the apples. We will make, in complete, 4 apple units:

- A bag with apple ivan,
`{ ivan }`

— units are written as curly brackets with the set objects inside. - Equally, you possibly can have a bag with apple bob,
`{ bob }`

. - A bag with each apples,
`{ ivan, bob }`

. Maintain onto your hats, that is referred to as a*universe*as a result of in the meanwhile there’s nothing in our world besides these two apples. - An empty bag aka empty set,
`{}`

. This one will get a particular image, ∅

Units are sometimes drawn as “venn diagrams”, with every set represented as a circle:

Other than itemizing all of the objects, we are able to additionally construct units by *situation.* I can say “R is a set of purple apples” to imply `{ ivan }`

, considernig ivan is purple and bob is inexperienced. Thus far, so good.

Set A is a *subset* of set B if each component from A can also be in B. In our apple world, `{ ivan }`

is a subset of `{ ivan, bob }`

, however `{ bob }`

shouldn’t be a subset of `{ ivan }`

. Clearly, any set is a subset of itself, and `{}`

is a subset of some other set S, as a result of not a single merchandise from `{}`

is lacking from S.

There are just a few helpful operators outlined on units:

- Union
*C = A ∪ B*accommodates all the weather which can be in A or in B. Notice that*A ∪ ∅ = A* - Intersection
*C = A ∩ B*accommodates all the weather which can be in A*and*B. Notice that*A ∪ ∅ = ∅* - Distinction
*C = A B*accommodates all the weather which can be in A, however not in B. Notice that*A ∅ = A*

This must be sufficient! Let’s have a look at the way it all maps to varieties.

## What does it need to do with varieties

So, the massive reveal: you possibly can consider “varieties” as units of JavaScript values. Then:

- Our
*universe*is all of the values a JS program can produce. - A kind (not even a typescript sort, only a sort usually) is a few set of JS values.
- Some varieties might be represented in TS, whereas different can’t — for instance, “non-zero numbers”.
`A extends B`

as seen in conditional types and generic constraints might be learn as “A is subset of B”.- Sort union,
`|`

, and intersection,`&`

, operators are simply the union and intersection of two units. `Exclude<A, B>`

is as shut as TS will get to a distinction operator, besides it*solely*works when each`A`

and`B`

are union varieties.`by no means`

is an empty set. Proof:`A & by no means = by no means`

and`A | by no means = A`

for any sort`A`

, and`Exclude<0, 0> = by no means`

.

This modification of view already yields some helpful insights:

- Subtype of sort A is a
*subset*of sort A. Supertype is a superset. Simple. *Widening*makes a type-set wider by permitting some further values.*Narrowing*removes sure values. Makes geometrical sense.

I do know this all appears like rather a lot, so let’s proceed by instance, beginning with a easy case of boolean values.

## Boolean varieties

For now, fake JS solely has boolean values. There are exaclty *two* — `true`

and `false`

. Recalling the apples, we are able to make a complete of 4 varieties:

- Literal varieties
`true`

and`false`

, every made up of a single worth; `boolean`

, which is*any*boolean worth;- The empty set,
`by no means`

.

The diagram of the “boolean varieties” is principally the one which we had for apples, simply the names swapped:

Let’s strive shifting between sort world and set world:

`boolean`

might be written as`true | false`

(the truth is, that is precisely how TS impements it).`true`

is a subset (aka sub-type) of`boolean`

`by no means`

is an empty set, so`by no means`

is a sub-set/sort of`true`

,`false`

, and`boolean`

`&`

is an*intersection,*so`false & true = by no means`

, and`boolean & true = (true | false) & true = true`

(the universe,`boolean`

, would not have an effect on intersections), and`true & by no means = by no means`

, and many others.`|`

is a*union,*so`true | by no means = true`

, and`boolean | true = boolean`

(the universe,`boolean`

, “swallows” different intersection objects as a result of they’re all subsets of universe).`Exclude`

accurately computes set distinction:`Exclude<boolean, true> -> false`

Now, somewhat self-assessment of the difficult `extends`

instances:

`sort A = boolean extends by no means ? 1 : 0;`

sort B = true extends boolean ? 1 : 0;

sort C = by no means extends false ? 1 : 0;

sort D = by no means extends by no means ? 1 : 0;

In the event you recall that “extends” might be learn as “is subset of”, the reply must be clear — A0,B1,C1,C1. We’re making progress!

`null`

and `undefined`

are similar to `boolean`

, besides they solely include *one* worth every. `by no means extends null`

nonetheless holds, `null & boolean`

is `by no means`

since no JS worth can concurrently be of two totally different JS varieties, and so forth. Let’s add these to our “trivial varieties map”:

## Strings and different primitives

With the straightforward ones out of the best way, let’s transfer on to string varieties. At first, it appears that evidently nothing’s modified — `string`

is a kind for “all JS strings”, and each string has a corresponding literal sort: `const str: 'hello' = 'hello';`

Nonetheless, there’s one key distinction — there are *infinitely many* doable string values.

It is likely to be a lie, as a result of you possibly can solely symbolize so many strings in finite pc reminiscence, however a) it is sufficient strings to make enumerating all of them unpractical, and b) sort programs

canfunction on pure abstrations with out worrying about soiled real-life limitations.

Identical to units, string varieties might be constructed in just a few other ways:

`|`

union permits you to constuct any*finite*string set — e.g.`sort Nation = 'de' | 'us';`

. This may not work for*infinite*units — say, all strings with size > 2 — since you possibly can’t write an infinite record of worth.- Funky template literal types allow you to assemble
*some*infinite units — e.g.`sort V = `v${string}`;`

is a set of all strings that begin with`v`

.

We will go a bit additional by making unions and intersections of literal and template varieties. Enjoyable time: when combining a *union* with a *template,* TS is sensible sufficient to simply filter the literals againts the template, in order that `'a' | 'b' & `a${string}` = 'a'`

. But, TS shouldn’t be *sensible sufficient* to merge templates, so that you get actually fancy methods of claiming `by no means`

, comparable to ``a${string}` & `b${string}``

(clearly, a string cannot begin with “a” and “b” on the identical time).

Nonetheless, some string varieties are *not* representable in TS in any respect. Attempt “each string besides ‘a'”. You can `Exclude<string, 'a'>`

, however since TS would not *truly* mannequin `string`

as *union of all doable string literals,* this the truth is evaluates again to `string`

. The template grammar can’t categorical this damaging situation both. Dangerous luck!

The categories for numbers, symbols and bigints work the identical means, besides they do not even get a “template” sort, and are restricted to finite units. It is a pity, as I might actually use some quantity subtypes — *integer, quantity between 0 and 1,* or *optimistic quantity.* In any case, all collectively:

Phew, we have coated all primitive, *non-intersecting* JS / TS varieties. We have gotten snug shifting between units and kinds, and found that some varieties cannot be outlined in TS. Right here comes the difficult half.

## Interfaces & object varieties

In the event you assume `const x: {} = 9;`

is not sensible, this part is for you. Because it seems, our psychological mannequin of what TS object varieties / data / interfaces was constructed on the unsuitable assumptions.

First, you’d in all probability count on varieties like `sort Sum9 = { sum: 9 }`

to behave like “literal varieties” for objects — matching a single object worth `{ sum: 9 }`

, adjusted for referential equality. That is completely *not* the way it works. As a substitute, `Sum9`

is a “*factor* on which you’ll entry propery `sum`

to get `9`

” — extra like a situation / constraint. This lets us name `(knowledge: Sum9) => quantity`

with an object `obj = { sum: 9, date: '2022-09-13' }`

with out TS complaining about unknown `date`

property. See, helpful!

Then, `{}`

sort shouldn’t be an “empty object” sort akin to a `{}`

JS literal, however a “factor the place I can entry properties, however I do not care about any explicit properties”. Aha, now we are able to see what is going on on in our preliminary mind-bender: if `x = 9`

, you possibly can safely `x['whatever']`

, so it satisfies the unconstrained `{}`

interface. The truth is, we are able to even make bolder claims like `const x: { toString(): string } = 9;`

, since we are able to `x.toString()`

and actuallty get a string. Extra but, `keyof quantity`

offers us `"toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"`

, which means that TS secretly sees our primitive sort as an object, which it’s (due to autoboxing). `null`

and `undefined`

don’t fulfill `{}`

, as a result of they throw in the event you attempt to learn a property. Not tremendous intuitive, however is sensible now.

Coming again to my little “`|`

or `&`

” downside, `&`

and `|`

function on “worth units”, not on “object shapes”, so that you want `{ identify: string } & { age: quantity }`

to get objects with *each* `identify`

and (further trace: and = `&`

) `age`

.

Oh, and what about that odd `object`

sort? Since each property on an interface simply provides a constraint to the “factor” we’re typing, there is no solution to declare an interface that filters out primitive values. It is why TS has a built-in `object`

sort which means particularly “JS object, not a primitive”. Sure, you possibly can intersect with `object`

to get solely *non-primitive* values satisfying an interface: `const x: object & { toString(): string } = 9`

fails.

Let’s add all of those to our sort map:

## extends

`extends`

key phrase in TS might be complicated. It comes from the object-oriented world the place you *prolong* a category within the sense of *including performance* to it, *however,* since TS makes use of structural typing, `extends`

as utilized in `sort Extends<A, B> = A extends B ? true : false`

is *not* the identical `extends`

from `class X extends Y {}`

.

As a substitute, `A extends B`

might be learn as *A is a sub-type of B* or, in set phrases, *A is a subset of B.* If B is a union, each member of A should even be in B. If B is a “constrained” interface, A should not violate any of B’s constraints. Excellent news: a regular OOP `class A extends B {}`

matches `A extends B ? 1 : 0`

. So does `'a' extends string`

, which means that (excuse the pun) TS `extends`

extends `extends`

.

This “subset” view is one of the best ways to by no means combine up the order of `extends`

operands:

`0 | 1 extends 0`

is fake, since a 2-element set`{0, 1}`

shouldn’t be a subset of the 1-element`{0}`

(despite the fact that`{0,1}`

does prolong`{1}`

in a geometrical sense).`by no means extends T`

is at all times true, as a result of`by no means`

, the empty set, is a subset of any set.`T extends by no means`

is just true if T is`by no means`

, as a result of an empty set has no subsets besides itself.`T extends string`

permits T to be a string, a literal, or a literal union, or a template, as a result of all of those are subsets of`string`

.`T extends string ? string extends T`

makes positive that T is*precisely*`string`

, as a result of that is the one means it may be each a subset*and*a superset of string.

## unknown and any

Typescript has two varieties that may symbolize an arbitrary JS worth — `unknown`

and `any`

. The traditional one is `unknown`

— the universe of JS values:

sort Y = string | quantity | boolean | object | bigint | image | null | undefined extends unknown ? 1 : 0;

sort Y2 = {} | null | undefined extends unknown ? 1 : 0;

sort N = unknown extends string ? 1 : 0;

On a puzzling facet, although:

`unknown`

is*not*a union of all different base varieties, so you possibly can’t`Exclude<unknown, string>`

`unknown extends string | quantity | boolean | object | bigint | image | null | undefined`

is fake, which means that some TS varieties are usually not listed. I think`enum`

s.

All in all, it is secure to consider `unknown`

as “the set of all doable JS values”.

`any`

is the bizarre one:

`any extends string ? 1 : 0`

evaluates to`0 | 1`

which is principally a “dunno”.- Even
`any extends by no means ? 1 : 0`

evaluates to`0 | 1`

, which means that`any`

*would possibly*be empty.

We should always conclude that `any`

is “some set, however we’re unsure which one” — like a kind `NaN`

. Nonetheless, upon additional inspection, `string extends any`

, `unknown extends any`

and even `any extends any`

are all true, none of which holds for “some set”. So, `any`

is a *paradox* — each set is a subset of `any`

, however `any`

*would possibly* be empty. The one excellent news I’ve is that `any extends unknown`

, so `unknown`

remains to be the universe, and `any`

doesn’t permit “alien” values.

So, to complete mapping our varieties, we wrap our total diagram into `unknown`

bubble:

At this time, we have learnt to that TS *varieties* are principally *units* of JS values. This is somewhat dictionary to go from type-world to set-world, and again:

- Our universe = all JS values = the kind
`unknown`

`by no means`

is an empty set.- Subtype = narrowed sort = subset, supertype = widened sort = superset.
`A extends B`

might be learn as “A is subset of B”.- Union and intersection varieties are, actually, simply set union and intersection.
`Exclude`

is an*approximation*of set distinction that solely works on union varieties.

Going again my our preliminary questions:

`0 | 1 extends 0`

is fake as a result of*{0,1}*is*not*a subset of*{0}*`&`

and`|`

work on units, not on object shapes.`A & B`

is a set of issues that fulfill each`A`

and`B`

.`unknown`

is the set of all JS values.`any`

is a paradoxical set that features every part, however*would possibly*even be empty.- Intersecting with
`by no means`

offers you`by no means`

as a result of it is an empty set.`by no means`

has no impact in a union. `const x: {} = true;`

works as a result of TS interfaces work by*constraining*the property values, and we have not constrained something right here, so`true`

matches.

We nonetheless have a number of TS mysteries to unravel, so keep tuned!