TypeScript Knowledge
Kip Landergren
(Updated: )
My TypeScript knowledge base explaining a quick overview and some key concepts.
Contents
- Overview
- Core Idea
- Key Concepts
- TypeScript Terminology
- Related
Overview
TypeScript is a static typing system for JavaScript. It works by defining a language that is a superset of JavaScript and providing a compiler, tsc
, that can output JavaScript. The resulting JavaScript can be tuned with compiler options to support previous versions of ECMAScript specifications. tsc
can also perform other static analysis on the input JavaScript to check for problems (e.g. implicit any
usage).
TypeScript works by appending type information after normal JavaScript code—like variables, function parameters, and functions—and then analyzing uses for type conformance.
Core Idea
Create a superset of JavaScript that allows for language extension, static typing, and compilation. Static typing can help improve readability and correctness guarantees. Additionally, tsc
gives opportunity to forward problem discovery to compile-time and have additional checks be able to be layered on.
Key Concepts
Generics
/* generics */
/* rationale */
/* consider the identity function, implemented using `any`: */
const identityAny = (arg: any) => {
return arg;
};
/* this does not get us type information of the argument we passed */
const a = identityAny("hello"); /* type: any */
const b = identityAny(100); /* type: any */
/* we could implement identity functions for all types */
const identityString = (arg: string) => {
return arg;
};
const identityNumber = (arg: number) => {
return arg;
};
const c = identityString("hello"); /* type: string */
const d = identityNumber(100); /* type: number */
/* but this requires a lot of redundant code! */
/* generic type parameters are type variables that we can use to specify types
in our function */
const identity = <T>(arg: T) => {
return arg;
};
/* our first invocations pass the type of `arg` as an argument to the type
parameter */
const e = identity<string>("hello"); /* type: string */
const f = identity<number>(100); /* type: number */
/* but TypeScript can actually infer what the type parameter is based on the
value of `arg`, so we can omit: */
const g = identity("hello"); /* type: string */
const h = identity(100); /* type: number */
/* how to think about type parameters */
/* generic type parameters are type variables you declare as "inputs" that must
have corresponding use as "outputs" either as argument types or return types */
const gen1 = <T, U>(arg1: T, arg2: U) => {};
const gen2 = <T, U>(arg1: T, arg2: U): T => {
return arg1;
};
export {};
Type Keywords
extends keyword
The extends
keyword is used as a predicate of the form:
U extends V
to check whether U
is assignable to V
. It is effectively asking: “Is the type to the left of extends
assignable to the one on the right?”.
extends
is used in two scenarios:
- as a type constraint, ensuring that in
T<U extends V>
U
is in fact assignable toV
; an error results otherwise - as part of a conditional type, combined with the ternary operator, to test
U extends V ? TrueType : FalseType
One can think of extends
use in conditional types as effectively being a type constraint on the TrueType
path.
as keyword (4.1 onward)
Used to remap types to a new type. Particularly useful in mapped types.
Type Operators
keyof T Type Operator
The operator keyof
iterates the properties of an object type T
and returns the string literal or numeric literal union of T
’s keys (enumerable property names). This is also known as an index type query.
/* keyof type operator - index type query */
type Person = {
name: string;
age: number;
};
/* keyof takes an object type T and returns the string literal or numeric
literal union of T's keys (enumerable properties) */
type K1 = keyof Person; /* "name" | "age" */
export {};
Types of Types
Union Types
A union type is formed by the symbol |
between multiple other types. Use this when the resulting type could be any of the members. Usage will be restricted to the types’ overlapping properties (unless type narrowing is used):
/* union types */
type StringOrNumber = string | number;
/* valid */
const s: StringOrNumber = "hello";
const n: StringOrNumber = 1;
/* error */
const a: StringOrNumber = ["whoops"];
/* using type narrowing */
const narrow = (x: string | number[]) => {
if (typeof x === "string") {
/* able to access to all of `string` properties */
return x.charAt(0);
}
/* able to access to all of `number[]` properties */
return x.lastIndexOf(0);
};
/* using overlapping properties */
const overlap = (x: string | number[]) => {
/* only able to access overlapping properties */
return x.slice(0, 2);
};
export {};
Intersection Types
An intersection type is formed by the symbol &
between multiple other types. Use this when all properties of the members are required.
/* intersection types */
type T0 = { age: number };
type T1 = { name: string };
const foo = (x: T0 & T1) => {
return `${x.name}: ${x.age}`;
};
export {};
Type Literals
Instead of using just the general types like string
or number
, we can use specific instances of those types—the literal values—like "GET"
or -1
or true
. This is especially useful when combined with unions to form types like "east" | "west"
or -1 | 0 | 1
which restrict the space of what a value can be.
Indexed Access Types (Lookup Types)
Let’s imagine you have:
type T = { bar: string }
You are able to extract the type of property bar
for use in the type system via type U = T["bar"]
.
For tuple types their values can be accessed as a new union type via T[number]
.
Mapped Types
A mapped type is one that has been transformed (mapped) from another.
/* mapped types */
type Person = {
name: string;
age: number;
};
/* let's imagine we want to create a type that has the same fields as Person,
but all are optional */
/* one attempt could be to recreate it with optional fields: */
type PartialPerson1 = {
name?: string;
age?: number;
};
/* but we can do better—the following protects against changes in Person
needing to cascade to PartialPerson. It uses the `keyof` operator to construct
a mapped type (`PartialPerson2`) from `Person` */
type PartialPerson2 = {
[P in keyof Person]?: Person[P];
};
/* in fact, this is so common there is already a built-in type we can use */
type PartialPerson3 = Partial<Person>;
/* let's look a little deeper at mapped types and indexed accessed types */
const tuple = ["foo", "bar", 1, 2] as const;
/* if we just use `keyof T`, this will go through every key (enumerable
property), which for an array will the numbers reflective of the index */
type A<T extends readonly (string | number)[]> = {
[P in keyof T]: P;
};
type APrime = A<typeof tuple>; /* readonly ["0", "1", "2", "3"] */
/* if we want to get the values referred to by those keys, we have to use an
indexed-access type T[number] to iterate through */
type B<T extends readonly (string | number)[]> = {
[P in T[number]]: P;
};
type BPrime = B<typeof tuple>; /* { foo: "foo"; bar: "bar"; 1: 1; 2: 2; } */
/* use of `as` to remap keys (TypeScript 4.1 onward): */
type T0<U> = {
[Key in keyof U as Key]: Key;
};
type T1<U, V> = {
[Key in keyof U as Key extends V ? never : Key]: Key;
};
type T2<U extends { countryCode: string }> = {
[Key in U as Key["countryCode"]]: Key;
};
type T3<U> = {
[Key in keyof U as `get${Capitalize<string & Key>}`]: Key;
};
export {};
Conditional Types
We can create conditionals within the type system of the form:
T extends U ? TrueType : FalseType
Which effectively says:
- if
T
is assignable toU
- return
TrueType
- return
- else
- return
FalseType
- return
Inference
The infer
keyword is used to introduce a new type variable where TypeScript is able to automatically infer its type. I am not exactly clear on how it works under the hood, or its exact limitations.
type Flatten<T> = T extends Array<infer Item> ? Item : T;
Distributive Conditional Types
When T
is a union type a conditional type will distribute its test over members of the union, forming a returned union type.
/* distributive conditional types */
/* conditional types distribute over union types. take for example: */
type MyExclude<T, U> = T extends U ? never : T;
/* when passed a union type, we get: */
type NoC = MyExclude<"a" | "b" | "c", "c">; /* "a" | b" */
/* this works (effectively) via:
*
* type NoC = MyExclude<"a" | "b" | "c", "c">;
* type NoC = MyExclude<"a", "c"> | MyExclude<"b", "c"> | MyExclude<"c", "c">;
* type NoC = "a" extends "c" ? never : "a" | "b" extends "c" ? never : "b" | "c" extends "c" ? never : "c" ;
* type NoC = "a" | "b" | never; */
export {};
Tuple Types
/* tuple types */
/* consider the following information encoded in an array: */
const f = ["north", 1]; /* type: (string | number)[] */
/* if we want to access a specific index of that information the type system
cannot really help us narrow down the type of the element we are requesting */
const f0 = f[0]; /* type: string | number */
const f1 = f[1]; /* type: string | number */
const f2 = f[2]; /* type: string | number */
/* contrast that behavior with the use of a "tuple type" where the array type
info is supplied at instantiation */
const g: [string, number] = ["east", 2]; /* type: [string, number] */
const g0 = g[0]; /* type: string */
const g1 = g[1]; /* type: number */
const g2 =
g[2]; /* error: Tuple type [string, number] of length '2' has no element at index '2' */
/* the spread operator can be used to define tuples with an unknown number of
elements, in any position: */
type T = [...string[], number, boolean];
type U = [string, ...number[], boolean];
type V = [string, number, ...boolean[]];
/* additionally, the spread operator works on generic type variables: */
type MyConcat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type W = MyConcat<
["1", 2],
[boolean, boolean]
>; /* ["1", 2, boolean, boolean] */
export {};
More info:
- TypeScript 4.0 Release Notes section on Variadic Tuple Types
- Variadic tuple types #39094 pull request
Specific Types
any
A type which will trigger no typechecking errors.
unknown
Same as any
but is only assignable to unknown
and any
. The type system will make it illegal to do anything with it without a type assertion or narrowing.
Introduced in the TypeScript 3.0 release notes.
never
A type which is / should never be observed. Useful within conditional types to ensure that the criteria is always met (if it wasn’t, never
will be assigned as the type).
TypeScript Terminology
- binding / bound
- the association of a specific object to
this
- falsy
- the quality of an object that evaluates to
false
- hoisting
- term used to provide a conceptual understanding of how variable and function declarations are put into memory during the compile phase; they can be thought of as being “hoisted” to the top of the file
- key
- an enumerable property of an object
- own property
- a property belonging to an object and not a member of its prototype chain
- truthy
- the quality of an object that evaluates to
true
- tuple
- an array for which the length is fixed and each index is typed