You got Set Theory in my Typescript [🌱]
element of
.
$3~{\in}~A$
namespace Element {
type A = number;
// 3 ∈ A
const e: A = 3;
}
not an element of
.
$4~{\notin}~B$
namespace NotElement {
type B = string;
// 4 ∉ B
const e: B = 3; // TS Error
}
subset
$A \subseteq B$
$A = \lbrace1,2,3\rbrace$
$B = \lbrace1,2,3\rbrace$
$A \subseteq B$
$X = \lbrace1,2\rbrace$
$Y = \lbrace1,2,3\rbrace$
$X \subseteq Y$
namespace Subset {
type A = 1 | 2 | 3;
type B = 1 | 2 | 3;
const a1: A = 1;
const a2: A = 2;
const a3: A = 3;
const b1: B = 1;
const b2: B = 2;
const b3: B = 3;
}
$A = \lbrace1,2\rbrace$
$B = \lbrace1,2,3\rbrace$
$A \subset B$
namespace ProperSubset {
type A = 1 | 2;
type B = 1 | 2 | 3;
const a1: A = 1;
const a2: A = 2;
const b1: B = 1;
const b2: B = 2;
// @ts-expect-error Type '3' is not assignable to type 'A'
const a3: A = 3;
}
If
not a subset
union
Unions of two sets is the set containing those 2 sets, so
namespace Unions {
type FirstThree = 'a' | 'b' | 'c';
type ThreeToSix = 'c' | 'd' | 'e' | 'f';
type FirstSixLetters = FirstThree | ThreeToSix;
// "a" | "b" | "c" | "d" | "e" | "f"
}
Any duplicate elements in the set are removed.
Intersecting a set
namespace Intersections {
type Evens = 2 | 4 | 6 | 8 | 10 | 12;
// Evens = {2, 4, 6, 8, 10, 12}
type Threes = 3 | 6 | 9 | 12;
// Threes = {3, 6, 9, 12}
// Intersecting a set A with a set B means extracting the part of A that also belongs to B.
// In other words, elements in common to both
type EvensThreeOverlap = Evens & Threes; // 6 | 12
// EvensThreeOverlap = Evens ∩ Threes
// {2, 4, 6, 8, 10, 12} ∩ {3, 6, 9, 12} = {6, 12}
const eto: EvensThreeOverlap = 6;
}
The result of intersecting types that do not overlap is the empty set. A set that does not contain anything. The empty set is called never
in TS.
type EmptySet = string & number;
// EmptySet = {} or EmptySet = Ø
There are no elements in an empty set, so there can be no elements in the empty set that aren't contained in the complete set. Therefore, the empty set is a subset of every set. In TS never
$\emptyset \subset A$
The union of any type
type A = 1 | 2 | 3 | 4 | 5;
type B = A | never;
// = A
$B = A \cup \emptyset$
$A = B$
If you intersect a type
type A = 1 | 2 | 3 | 4 | 5;
type B = A & never;
// = never
A Universal Set is the set of all elements under consideration. All other sets are subsets of the universal set. The Universal Set contains each and every type you will ever use in TypeScript. And is expressed as unknown
.
$A \subset \textit{U}$
$B \subset \textit{U}$
$C \subset \textit{U}$
The union of any type
type A = 1 | 2 | 3 | 4 | 5;
type B = A | unknown;
// = unknown
If you intersect a type
type A = 1 | 2 | 3 | 4 | 5;
type B = A & unknown;
// = A
Before getting started, some ground work is needed. In Typescript there are some common categories of types. Some, like literals type a = 42
, can be subsets of others, like number
. Unions and Intersections also fall under this heading.
type Primitives =
| number
| string
| boolean
| symbol
| bigint
| undefined
| null;
// A = {all numbers}
// B = {all strings}
// C = {true, false}
// ...
// Literals
type Literals =
| 42
| 'Typescript'
| true;
type DataStructures =
| { key1: boolean; key2: number } // objects
| { [key: string]: number } // records
| [boolean, number] // tuples
| number[]; // arrays
// Object types describe objects with a finite set of keys, and these keys
// contain values of potentially different types.
// Record types are similar to object types, except they describe objects with
// an unknown number of keys, and all values
// in a record share the same type.
// For example, in { [key: string]: number }, all values are numbers.
// Tuple types describe arrays with a fixed length. They can have a different type for each index.
// Array types describe arrays with an unknown length.
// Just like with records, all values share the same type.
Type objects, like the JS object they represent, are structured in the same way. And, also like the JS object, can have as many properties as is needed. Indexed by unique keys.
type Motorcycle = {
make: string;
model: string;
year: number;
}
const Tuono: Motorcycle = {
make: 'Aprilia',
model: 'Tuono 660',
year: 2021,
};
Since we are defining these objects inline TS doesnt let us add extra types as we wouldnt be able to reference those extra props afterwards due to the type.
const Rs: Motorcycle = {
make: 'Aprilia',
model: 'RSV4',
year: 2021,
// @ts-expect-error Object literal may only specify known properties
engine: 'V4',
};
But if, for instance, we were getting those from an API response we would be able to assign them to Motorcycle (though we still wouldnt be able to use the extra). So an object type is the set of objects with at least all properties it defines.
Prop types can be accessed similar to accessing prop values using bracket ([]) syntax. Trying to use dot notation will throw however.
type Make = Motorcycle['make']; // string
And since the "value" in the bracket is just a string literal, unions also apply.
type ModelOrYear = Motorcycle['model' | 'year']; // string | number
This is the same as accessing them each seperately.
type MakeOrYear = Motorcycle['make'] | Motorcycle['year']; // string | number
keyof
functions like Object.keys
for the object types.
type Keys = keyof Motorcycle; // 'make' | 'model' | 'year'
And, like above, since they are string literals, you can use them to access the types.
type MotorcycleValues = Motorcycle[keyof Motorcycle]; // string | number
This pattern can be refactored to a generic for easy reuse
type ValueOf<T> = T[keyof T];
type MotoValues = ValueOf<Motorcycle>; // string | number
Merging objects using the intersection symbol, unlike the normal behavior of
extracting the part of A that also belongs to B
when used with objects, it acts more like { ...A, ...B}
.
type Name = { name: string };
type Age = { age: number };
type NameAndAge = Name & Age; // { name: string } & { age: number };
const user: NameAndAge = { name: 'Fred', age: 32 };
// @ts-expect-error Property 'age' is missing in type '{ name: string; }'
const userName: NameAndAge = { name: 'Fred' };
// @ts-expect-error Property 'name' is missing in type '{ age: number; }'
const userAge: NameAndAge = { age: 32 };
But why though? With objects, you are not intersecting their keys, but their subtyping set. Since you can assign object types with additional keys ({ a: string, b: number, c: Date }
) to object types with fewer keys ({ a: string, b: number }
) -- caveat, as long as it isnt done inline -- so there could be objects { a: string, b: number }
that contain a c: Date
. So, intersecting objects returns the set of values that belong to both sets.
type KeyOfName = keyof Name; // 'name'
type KeyOfAge = keyof Age; // 'age'
type KeyOfNameAge = keyof NameAndAge; // 'age' | 'name'
Caveat for duplicates. In the event that two objects contain the same prop of differing types (non-subsets), the resulting property type will be never
.
type A = { a: string, b: number };
type B = { b: string, c: number };
type C = A & B;
// { a: string, b: never, c: number }
// @ts-expect-error "Type 'number' is not assignable to type 'never'." For prop 'b'
const tm: CMerge = { a: '', b: 0, c: 0 };
And the union of two objects is the opposite
type X = { name: string, height: number };
type Y = { name: string, age: number };
type KeyOfX = keyof X; // 'name' | 'height'
type KeyOfY = keyof Y; // 'name' | 'age'
type Z = X | Y;
type KeyOfZ = keyof Z; // 'name'
If that doesn't make sense, the TL:DR is the intersection of two objects is the union of their keys and the union of two objects is the intersecction of their keys. Additionally, Unions and Intersections of objects aren't as performant as Interfaces. Though Interfaces can only be defined statically.
Records are like Objects with the exception that all of the values, of all of the props, must be of the same type.
type NamesRecord = { [key: string]: string };
There is also a built in generic for this if prefered.
type AgeRecord = Record<string, number>;
The key for Records can also be a union.
type AddressRecord = { [key in 'street' | 'city' | 'state']: string };
which equates to
type AddressObj = {
street: string;
city: string;
state: string;
}
And because of this, we can get the types of the props just like normal objects using []
. Though if the generic records are used, all of the props have the same value type and it can be simplified.
type TypeOfNamesRecord = NamesRecord[string]; // string
type TypeOfAgeRecord = AgeRecord[string]; // number
This simultaneously reads all keys assignable to the type string
. Since all of them are the same type, you get that type back.
Partial takes an object type and returns a similar object type with all of the property types as optional props { a: string } → { a?: string }
.
type A = { name: string, age: number };
type PartialA = Partial<A>;
// { name?: string | undefined, age?: number | undefined }
Required, like Partial, takes an object type but, instead, returns a similar object type with all of the property types as required props { a?: string } → { a: string }
.
type B = { name?: string, age?: number };
type RequiredB = Required<B>;
// { name: string, age: number }
ReadOnly, like the above, also takes an object type and returns a similar object type. And as you have probably guess, the difference is that all of the props are readonly.
type A = { name: string, age: number };
type ReadOnlyA = Readonly<A>;
// { readonly name: string, readonly age: number }
Pick, those familiar with lodash will probably recognize this. It takes an object type and a key literal (or union of literals) and returns an object type with just those props.
type C = { name: string, age: number, height: number, weight: number };
type PickName = Pick<C, 'name'>;
// { name: string }
type PickHeightWeight = Pick<C, 'height' | 'weight'>;
// { height: number, weight: number }
Omit is similar to Pick except it does the opposite. It returns an object type with the given keys removed.
type C = { name: string, age: number, height: number, weight: number };
type OmitName = Omit<C, 'name'>;
// { age: number, height: number, weight: number }
type OmitHeightWeight = Omit<C, 'height' | 'weight'>;
// { name: string, age: number }