TypeScript Knowledge

Kip Landergren

(Updated: )

My TypeScript knowledge base explaining a quick overview and some key concepts.

Contents

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:

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:

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:

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

Related