Skip to content

Instantly share code, notes, and snippets.

@ruizb
Last active January 17, 2025 13:06
Show Gist options
  • Select an option

  • Save ruizb/55e1fc37cb198dccfdaf81450c3ebd43 to your computer and use it in GitHub Desktop.

Select an option

Save ruizb/55e1fc37cb198dccfdaf81450c3ebd43 to your computer and use it in GitHub Desktop.
A glossary of TypeScript.

A glossary of TypeScript

Motivation

Once upon a time, there was a developer that had an issue using the TypeScript language. He wanted to share his issue with the community to get help, but he didn't know how to properly write the title that would best describe his problem. He struggled to find the appropriate words as he didn't know how to name "this behavior" or "that kind of type mechanism".

This story encouraged me to start writing a glossary of TypeScript. Hopefully it will help you if you have to look for an issue, write an issue, or communicate with other TypeScript developers.

Disclaimer: it was me, I was the developer that struggled. I still struggle though, but this means I still have things to learn, which is great!

You may not agree with some definitions or examples in this document. Please, feel free to leave a comment and let me know, only together can we improve this glossary! Also, if the stars are aligned and this gist gets more and more attention, I might move it into its own repository so we can all contribute via issues and pull-requests! 🚀

Table on content

Glossary

Conditional type

Conditional types were introduced in TypeScript v2.8. They can be used to make non-uniform type mapping, meaning they can be considered as functions for types, like mapped types but more powerful.

TypeScript provides several conditional types for the developers, such as Exclude, NonNullable, Parameters or ReturnType.

Example
// { "strict": true }

const isDefined = <A>(value: A | null | undefined): value is NonNullable<A> =>
  value !== null && value !== undefined

const button = document.getElementById('my-button') // HTMLElement | null
if (isDefined(button)) {
  console.log(`Button text: ${button.innerText}`)
} else {
  console.log('Button is not defined')
}

In this example we are using the conditional type NonNullable in strict mode as a type guard to make sure the value is defined in the "true" branch of our condition.

Example
type SyncOperationName = 'getViewportSize' | 'getElementSize'
type AsyncOperationName = 'loadUsers' | 'sendData'
type OperationName = SyncOperationName | AsyncOperationName

type Operation<A extends OperationName> = A extends SyncOperationName
  ? { type: 'syncOperation', name: A }
  : { type: 'asyncOperation', name: A, timeout: number }

Here we are mapping A to a completely different type Operation<A>, which depends on the effective type of A:

  • If A is a SyncOperationName then Operation<A> is { type: 'syncOperation', name: A }.
  • Otherwise, since A is a OperationName, OperationName is a union type and the first element of that union type has already been checked with A extends SyncOperationName, then A must be a AsyncOperationName in the "else" branch of this ternary expression. Therefore, Operation<A> becomes { type: 'asyncOperation', name: A, timeout: number }.

This is a very powerful feature that allows building complex types, especially when using type inference in conditional types.

You can check the official documentation for more information about conditional types.

Diagnostic message

When TypeScript detects an error/warning in your program, its checker (an internal component of the language) generates a diagnostic message.

Example
// { "strict": true }

const value: string = undefined
Type 'undefined' is not assignable to type 'string'. (2322)

The 2322 number is an ID that could be used to reference the type of diagnostic message when creating an issue for example. More specifically, the 2322 ID relates to the Type '{0}' is not assignable to type '{1}'. diagnostic. You can also check the wiki for more information about these messages.

Discriminated union

Discriminated unions were introduced in TypeScript 2.0 as "tagged union types".

Example
interface Square {
  kind: 'square'
  size: number
}

interface Rectangle {
  kind: 'rectangle'
  width: number
  height: number
}

interface Circle {
  kind: 'circle'
  radius: number
}

type Shape = Square | Rectangle | Circle

Here, Shape is the discriminated union, where the discriminant - also known as singleton property or tag - is the kind property.

You can also check the official documentation section on discriminated unions.

Intersection type

Intersection types were introduced in TypeScript v1.6 as a complement to union types. This allows us to type a value that is both a A and a B.

Example
interface WithName {
  name: string
}

interface WithAge {
  age: number
}

type User = WithName & WithAge

The User type is computed as { name: string, age: number }, which is the intersection of both types WithName and WithAge.

The intersection of types that have nothing in common results in the never type, introduced in TypeScript v2.0.

Example
type A = 'a' & 'b'

type B = { name: string, age: number } & { name: number, adult: boolean }

Here, A is never, because the string literals 'a' and 'b' have nothing in common.

For B, its type results in { name: never, age: number, adult: boolean } because string and number have nothing in common. As one of its properties type is never, there's no way to create a value which type is B, because no value can be assigned to the never type.

Literal type

Literal types were introduced in TypeScript v1.8. They are meant to expect only a specific set of strings, numbers or boolean, instead of "any string, number or boolean".

Example
type Scheme = 'http' | 'https'

const prependWithScheme = (scheme: Scheme, domain: string, path: string): string =>
  `${scheme}://${domain}/${path}`

prependWithScheme('http')
prependWithScheme('https')
prependWithScheme('')

Here, no TypeScript error is raised for prependWithScheme('http') and prependWithScheme('https'). Even better, the IDE knows which values are available for autocompletion. However, an error is raised for prependWithScheme('') as the empty string '' is not available in the string literal type Scheme.

Example
interface SuccesfulResponse {
  successfulRequest: true
  status: 200 | 201 | 202
  data: unknown
}

interface UnsuccesfulResponse {
  successfulRequest: false
  status: 400 | 500
  errorMessage: string
}

const handleResponse = (response: SuccesfulResponse | UnsuccesfulResponse): void => {
  if (response.successfulRequest) {
    console.log(`Status: ${response.status}, data: ${response.data}`)
  } else {
    console.log(`Status: ${response.status}, error message: ${response.errorMessage}`)
  }
}

Mapped type

Mapped types were introduced in TypeScript v2.1. They can be used to make uniform type mapping, allowing a developer to transform a type into another type of the same "form":

  • A mapped type can use a string or subset of a string (i.e. string literal type) to build an object thanks to the built-in Record mapped type:

    type A = Record<'firstname' | 'lastname' | 'surname', string>
    /**
     * {
     *   firstname: string,
     *   lastname: string,
     *   surname: string
     * }
     */
  • A mapped type can be a subset of the original type, e.g. using Pick or Omit (cf. example below).

  • A mapped type can change the optionality of all the properties of the original type using Required, Nullable or Partial.

  • A mapped type can change the visibility of all the properties of the original type using Readonly.

Example
interface Config {
  address: string
  port: number
  logsLevel: 'none' | 'error' | 'warning' | 'info' | 'debug'
}

type A = Partial<Config>
type B = Omit<Config, 'address' | 'logsLevel'>
type C = Pick<Config, 'logsLevel'>
type D = Readonly<Config>

Types A, B, C and D are all uniform mappings of Config thanks to the built-in mapped types Partial, Omit, Pick and Readonly:

  • A is computed as:

    {
      address?: string | undefined,
      port?: number | undefined,
      logslevel?: 'none' | 'error' | 'warning' | 'info' | 'debug' | undefined
    }
  • B is computed as:

    {
      port: number
    }
  • C is computed as:

    {
      logslevel: 'none' | 'error' | 'warning' | 'info' | 'debug' | undefined
    }
  • D is computed ad:

    {
      readonly address: string,
      readonly port: number,
      readonly logslevel: 'none' | 'error' | 'warning' | 'info' | 'debug'
    }

There is a section in the official documentation about mapped types.

Type alias

A type alias attaches a name to the definition of a type, whatever its complexity. In addition to naming function and object types like interfaces, type aliases can be used to name primitives, unions, tuples... Whatever type actually.

Example
type Name = string
type LazyName = () => string

interface User_1 {
  name: string
  age: number
}

type User_2 = {
  name: string
  age: number
}

interface Predicate_1<A> {
  (value: A): boolean
}

type Predicate_2<A> = (value: A) => boolean

You can go to the official documentation regarding type aliases for more information.

Type guard

A type guard, whether it's built into the TypeScript language or provided by the developer, allows for a type to be narrowed in a conditional branch.

Example
declare const value: string | number

const getNumberOfChars = (s: string): number => s.length

const isGreaterThan = (n: number, bound: number): boolean => n > bound

if (typeof value === 'string') {
  console.log(`Number of chars in ${value}: ${getNumberOfChars(value)}.`)
} else if (typeof value === 'number') {
  console.log(`Is ${value} greater than 20? ${isGreaterThan(value, 20) ? 'Yes' : 'No'}.`)
} else {
  throw 'Impossible, perhaps the archives are incomplete'
}

By checking the type of value with typeof, TypeScript knows that in the if branch, value must have that type and not the others:

  • In the if (typeof value === 'string') { ... } branch, value type is narrowed from string | number to string.
  • In the if (typeof value === 'number') { ... } branch, value type is narrowed from string | number to number.
  • In the else { ... } branch, since all the possible types have been checked in the previous if conditions, value type is narrowed from string | number to never (it "can't" happen since all the possible cases have been checked already).

The developer can also create its own type guards thanks to the is syntax.

Example
interface User {
  name: string
  age: number
}

// You can ignore this function for the sake of this example
const hasOwnProperty = <A extends {}, B extends PropertyKey>(obj: A, prop: B): obj is A & Record<B, unknown> =>
  obj.hasOwnProperty(prop)

const isUser = (v: unknown): v is User =>
  typeof v === 'object' &&
  v !== null &&
  hasOwnProperty(v, 'name') &&
  typeof v.name === 'string' &&
  hasOwnProperty(v, 'age') &&
  typeof v.age === 'number'

declare const value: unknown

if (isUser(value)) {
  console.log(`User(name = ${value.name}, age = ${value.age})`)
} else {
  throw `Invalid user provided: ${JSON.stringify(value)}`
}

We created the v is User type guard as the return value of the isUser function, which tells TypeScript that if isUser(value) returns true, then value is a User in the if branch, otherwise keep the initial type, which is unknown here.

You can also check the official documentation about type guards.

Type inference

Type inference is the ability for the TypeScript compiler to appropriately guess the type of a value, without manually specifying the type for that value.

Example
const username = 'Bob'
const port = 8080
const names = ['Bob', 'Henri', 'Elizabeth']
const finiteNames = ['Bob', 'Henri', 'Elizabeth'] as const
  • Type of username is the string literal type 'Bob'
  • Type of port is the number literal type 8080
  • Type of names is string[]
  • Type of finiteNames is readonly ['Bob', 'Henri', 'Elizabeth'], in other words a readonly tuple of 3 elements

Type narrowing

It's the ability for the TypeScript language to restrict the type of a value to a subset of that type.

Example
type A =
  | { kind: 'a', arg1: 'hey' }
  | { kind: 'b', arg1: 'Hello', arg2: 'World' }
  | { kind: 'c' }

declare const value: A

if (value.kind === 'a') {
  console.log(`Value of kind ${value.kind} has argument ${value.arg1}`)
} else if (value.kind === 'b') {
  console.log(`Value of kind ${value.kind} has arguments ${value.arg1} and ${value.arg2}`)
} else {
  console.log(`Value of kind ${value.kind} has no argument`)
}
  • If value.kind === 'a' is true, then value type is { kind: 'a', arg1: 'hey' }
  • Otherwise, if value.kind === 'b' is true, then value type is { kind: 'b', arg1: 'Hello', arg2: 'World' }
  • Otherwise, since we've already handled the cases { kind: 'a', arg1: 'hey' } and { kind: 'b', arg1: 'Hello', arg2: 'World' }, only the last one of the discriminated union A is left, i.e. { kind: 'c' }

More examples are available in the type guard section.

Union type

Union types were introduced in TypeScript v1.4. It's a way of expressing mulitple possible types for a given value.

Example
declare const value: string | number | boolean

Here, value type can be either string, number or boolean.

@nousernames2
Copy link
Copy Markdown

Thanks for taking the time to put this together. +1

@Ellen010
Copy link
Copy Markdown

Valuable resource, very apprecited!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment