GeistHaus
log in · sign up

Marius Schulz

Part of feedburner.com

Marius Schulz on web development, modern JavaScript, and TypeScript.

stories
Keyboard Shortcuts for Jumping and Deleting in iTerm2
How to configure familiar keyboard shortcuts for common navigation and edit actions in the iTerm2 command prompt, such as jumping to the start or end of a word or line.
Show full content

As a software engineer, I find myself using the terminal every day to run all sorts of commands. My current terminal of choice is iTerm2 which I’ve been using happily for many years.

Whenever I set up iTerm2 on a new Mac, one of the first things I do is to configure familiar keyboard shortcuts for common navigation and edit actions in the command prompt. For example, I typically configure the ⌥← shortcut which jumps me to the start of the word under the cursor.

To configure custom keyboard shortcuts in iTerm2, open the preferences dialog and navigate to the Profiles › Keys › Key Mappings tab:

iTerm2 Preferences: Profiles › Keys › Key Mappings

Click the “+” button to add a new key mapping, or double-click an existing key mapping to edit it. For the “Jump to start of word” command, select the “Send Escape Sequence” action and send the escape sequence Esc+b:

iTerm2 Preferences: dialog for editing a key mapping

Now, whenever you're typing a command in iTerm2, it's really easy to jump back to the start of the word (or even multiple words) to insert more text or delete part of the command — no more need for repeatedly pressing the ← key to navigate back character by character.

Here's the full list of keyboard shortcuts that I configure for various jump and delete commands:

Shortcut Command Action Send ⌥← Jump to start of word Send Escape Sequence b ⌥→ Jump to end of word Send Escape Sequence f ⌘← Jump to start of line Send Hex Codes 0x01 ⌘→ Jump to end of line Send Hex Codes 0x05 ⌥⌫ Delete to start of word Send Hex Codes 0x17 ⌘⌫ Delete entire line Send Hex Codes 0x15
urn:uuid:cc49d6fe-336c-427a-901d-3bbf9ff4d246
Extensions
Assertion Functions in TypeScript
TypeScript 3.7 implemented support for assertion functions in the type system. An assertion function is a function that throws an error if something unexpected happened. Using assertion signatures, we can tell TypeScript that a function should be treated as an assertion function.
Show full content

TypeScript 3.7 implemented support for assertion functions in the type system. An assertion function is a function that throws an error if something unexpected happened. Using assertion signatures, we can tell TypeScript that a function should be treated as an assertion function.

An Example: The document.getElementById() Method

Let's start by looking at an example in which we're using the document.getElementById() method to find a DOM element that has the ID "root":

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

We're calling the root.addEventListener() method to attach a click handler to the element. However, TypeScript reports a type error:

const root = document.getElementById("root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

The root variable is of type HTMLElement | null, which is why TypeScript reports a type error "Object is possibly null" when we're trying to call the root.addEventListener() method. In order for our code to be considered type-correct, we somehow need to make sure that the root variable is non-null and non-undefined before calling the root.addEventListener() method. We have a couple of options for how we can do that, including:

  1. Using the non-null assertion operator !
  2. Implementing an inline null check
  3. Implementing an assertion function

Let's look at each of the three options.

Using the Non-Null Assertion Operator

First up, we'll try and use the non-null assertion operator ! which is written as a post-fix operator after the document.getElementById() call:

const root = document.getElementById("root")!;

root.addEventListener("click", e => {
  /* ... */
});

The non-null assertion operator ! tells TypeScript to assume that the value returned by document.getElementById() is non-null and non-undefined (also known as “non-nullish”). TypeScript will exclude the types null and undefined from the type of the expression to which we apply the ! operator.

In this case, the return type of the document.getElementById() method is HTMLElement | null, so if we apply the ! operator, we get HTMLElement as the resulting type. Consequently, TypeScript no longer reports the type error that we saw previously.

However, using the non-null assertion operator is probably not the right fix in this situation. The ! operator is completely erased when our TypeScript code is compiled to JavaScript:

const root = document.getElementById("root");

root.addEventListener("click", e => {
  /* ... */
});

The non-null assertion operator has no runtime manifestation whatsoever. That is, the TypeScript compiler does not emit any validation code to verify that the expression is actually non-nullish. Therefore, if the document.getElementById() call returns null because no matching element can be found, our root variable will hold the value null and our attempt to call the root.addEventListener() method will fail.

Implementing an Inline Null Check

Let's now consider the second option and implement an inline null check to verify that the root variable holds a non-null value:

const root = document.getElementById("root");

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

root.addEventListener("click", e => {
  /* ... */
});

Because of our null check, TypeScript's type checker will narrow the type of the root variable from HTMLElement | null (before the null check) to HTMLElement (after the null check):

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

if (root === null) {
  throw Error("Unable to find DOM element #root");
}

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

This approach is much safer than the previous approach using the non-null assertion operator. We're explicitly handling the case in which the root variable holds the value null by throwing an error with a descriptive error message.

Also, note that this approach does not contain any TypeScript-specific syntax whatsoever; all of the above is syntactically valid JavaScript. TypeScript's control flow analysis understands the effect of our null check and narrows the type of the root variable in different places of the program — no explicit type annotations needed.

Implementing an Assertion Function

Lastly, let's now see how we can use an assertion function to implement this null check in a reusable way. We'll start by implementing an assertNonNullish function that will throw an error if the provided value is either null or undefined:

function assertNonNullish(
  value: unknown,
  message: string
) {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

We're using the unknown type for the value parameter here to allow callsites to pass a value of an arbitrary type. We're only comparing the value parameter to the values null and undefined, so we don't need to require the value parameter to have a more specific type.

Here's how we would use the assertNonNullish function in our example from before. We're passing it the root variable as well as the error message:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

However, TypeScript still produces a type error for the root.addEventListener() method call:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

// Object is possibly null
root.addEventListener("click", e => {
  /* ... */
});

If we have a look at the type of the root variable before and after the assertNonNullish() call, we'll see that it is of type HTMLElement | null in both places:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement | null
root;

root.addEventListener("click", e => {
  /* ... */
});

This is because TypeScript doesn't understand that our assertNonNullish function will throw an error if the provided value is nullish. We need to explicitly let TypeScript know that the assertNonNullish function should be treated as an assertion function that asserts that the value is non-nullish, and that it will throw an error otherwise. We can do that using the asserts keyword in the return type annotation:

function assertNonNullish<TValue>(
  value: TValue,
  message: string
): asserts value is NonNullable<TValue> {
  if (value === null || value === undefined) {
    throw Error(message);
  }
}

First of all, note that the assertNonNullish function is now a generic function. It declares a single type parameter TValue that we use as the type of the value parameter; we're also using the TValue type in the return type annotation.

The asserts value is NonNullable<TValue> return type annotation is what's called an assertion signature. This assertion signature says that if the function returns normally (that is, if it doesn't throw an error), it has asserted that the value parameter is of type NonNullable<TValue>. TypeScript uses this piece of information to narrow the type of the expression that we passed to the value parameter.

The NonNullable<T> type is a conditional type that is defined in the lib.es5.d.ts type declaration file that ships with the TypeScript compiler:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

When applied to the type T, the NonNullable<T> helper type removes the types null and undefined from T. Here are a few examples:

  • NonNullable<HTMLElement> evaluates to HTMLElement
  • NonNullable<HTMLElement | null> evaluates to HTMLElement
  • NonNullable<HTMLElement | null | undefined> evaluates to HTMLElement
  • NonNullable<null> evaluates to never
  • NonNullable<undefined> evaluates to never
  • NonNullable<null | undefined> evaluates to never

With our assertion signature in place, TypeScript now correctly narrows the type of the root variable after the assertNonNullish() function call. The type checker understands that when root holds a nullish value, the assertNonNullish function will throw an error. If the control flow of the program makes it past the assertNonNullish() function call, the root variable must contain a non-nullish value, and therefore TypeScript narrows its type accordingly:

const root = document.getElementById("root");

// Type: HTMLElement | null
root;

assertNonNullish(root, "Unable to find DOM element #root");

// Type: HTMLElement
root;

root.addEventListener("click", e => {
  /* ... */
});

As a result of this type narrowing, our example now type-checks correctly:

const root = document.getElementById("root");
assertNonNullish(root, "Unable to find DOM element #root");

root.addEventListener("click", e => {
  /* ... */
});

So here we have it: a reusable assertNonNullish assertion function that we can use to verify that an expression has a non-nullish value and to narrow the type of that expression accordingly by removing the null and undefined types from it.

urn:uuid:6d5e9c03-3be4-4c6b-908f-ff28e983a865
Extensions
Optional Chaining: The ?. Operator in TypeScript
TypeScript 3.7 added support for the ?. operator, also known as the optional chaining operator. We can use this operator to descend into an object whose properties potentially hold the values null or undefined without writing any null checks for intermediate properties.
Show full content

TypeScript 3.7 added support for the ?. operator, also known as the optional chaining operator. We can use optional chaining to descend into an object whose properties potentially hold the values null or undefined without writing any null checks for intermediate properties.

Optional chaining is not a feature specific to TypeScript. The ?. operator got added to the ECMAScript standard as part of ES2020. All modern browsers natively support optional chaining (not including IE11).

In this post, I will go over the following three optional chaining operators and explain why we might want to use them in our TypeScript or JavaScript code:

  • ?.
  • ?.[]
  • ?.()
Motivation

Let's start by looking at a real-world example in which optional chaining comes in handy. I've defined a serializeJSON function that takes in any value and serializes it as JSON. I'm passing a user object with two properties to the function:

function serializeJSON(value: any) {
  return JSON.stringify(value);
}

const user = {
  name: "Marius Schulz",
  twitter: "mariusschulz",
};

const json = serializeJSON(user);

console.log(json);

The program prints the following output to the console:

{"name":"Marius Schulz","twitter":"mariusschulz"}

Now let's say that we want to let callers of our function specify the indentation level. We'll define a SerializationOptions type and add an options parameter to the serializeJSON function. We'll retrieve the indentation level from the options.formatting.indent property:

type SerializationOptions = {
  formatting: {
    indent: number;
  };
};

function serializeJSON(value: any, options: SerializationOptions) {
  const indent = options.formatting.indent;
  return JSON.stringify(value, null, indent);
}

We can now specify an indentation level of two spaces when calling serializeJSON like this:

const user = {
  name: "Marius Schulz",
  twitter: "mariusschulz",
};

const json = serializeJSON(user, {
  formatting: {
    indent: 2,
  },
});

console.log(json);

As we would expect, the resulting JSON is now indented with two spaces and broken across multiple lines:

{
  "name": "Marius Schulz",
  "twitter": "mariusschulz"
}

Typically, options parameters like the one we introduced here are optional. Callers of the function may specify an options object, but they're not required to. Let's adjust our function signature accordingly and make the options parameter optional by appending a question mark to the parameter name:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options.formatting.indent;
  return JSON.stringify(value, null, indent);
}

Assuming we have the --strictNullChecks option enabled in our TypeScript project (which is part of the --strict family of compiler options), TypeScript should now report the following type error in our options.formatting.indent expression:

Object is possibly 'undefined'.

The options parameter is optional, and as a result it might hold the value undefined. We should first check whether options holds the value undefined before accessing options.formatting, otherwise we risk getting an error at runtime:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options !== undefined
    ? options.formatting.indent
    : undefined;
  return JSON.stringify(value, null, indent);
}

We could also use a slightly more generic null check instead that will check for both null and undefined — note that we're deliberately using != instead of !== in this case:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options != null
    ? options.formatting.indent
    : undefined;
  return JSON.stringify(value, null, indent);
}

Now the type error goes away. We can call the serializeJSON function and pass it an options object with an explicit indentation level:

const json = serializeJSON(user, {
  formatting: {
    indent: 2,
  },
});

Or we can call it without specifying an options object, in which case the indent variable will hold the value undefined and JSON.stringify will use a default indentation level of zero:

const json = serializeJSON(user);

Both function calls above are type-correct. However, what if we also wanted to be able to call our serializeJSON function like this?

const json = serializeJSON(user, {});

This is another common pattern you'll see. Options objects tend to declare some or all of their properties as optional so that callers of the function can specify as many (or as few) options as needed. We need to make the formatting property in our SerializationOptions type optional in order to support this pattern:

type SerializationOptions = {
  formatting?: {
    indent: number;
  };
};

Notice the question mark after the name of the formatting property. Now the serializeJSON(user, {}) call is type-correct, but TypeScript reports another type error when accessing options.formatting.indent:

Object is possibly 'undefined'.

We'll need to add another null check here given that options.formatting could now hold the value undefined:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options != null
    ? options.formatting != null
      ? options.formatting.indent
      : undefined
    : undefined;
  return JSON.stringify(value, null, indent);
}

This code is now type-correct, and it safely accesses the options.formatting.indent property. These nested null checks are getting pretty unwieldy though, so let's see how we can simplify this property access using the optional chaining operator.

The ?. Operator: Dot Notation

We can use the ?. operator to access options.formatting.indent with checks for nullish values at every level of this property chain:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.indent;
  return JSON.stringify(value, null, indent);
}

The ECMAScript specification describes optional chaining as follows:

Optional chaining [is] a property access and function invocation operator that short-circuits if the value to access/invoke is nullish.

The JavaScript runtime evaluates the options?.formatting?.indent expression as follows:

  • If options holds the value null or undefined, produce the value undefined.
  • Otherwise, if options.formatting holds the value null or undefined, produce the value undefined.
  • Otherwise, produce the value of options.formatting.indent.

Note that the ?. operator always produces the value undefined when it stops descending into a property chain, even when it encounters the value null. TypeScript models this behavior in its type system. In the following example, TypeScript infers the indent local variable to be of type number | undefined:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.indent;
  return JSON.stringify(value, null, indent);
}

Thanks to optional chaining, this code is a lot more succinct and just as type-safe as before.

The ?.[] Operator: Bracket Notation

Next, let's now look at the ?.[] operator, another operator in the optional chaining family.

Let's say that our indent property on the SerializationOptions type was called indent-level instead. We'll need to use quotes to define a property that has a hyphen in its name:

type SerializationOptions = {
  formatting?: {
    "indent-level": number;
  };
};

We could now specify a value for the indent-level property like this when calling the serializeJSON function:

const json = serializeJSON(user, {
  formatting: {
    "indent-level": 2,
  },
});

However, the following attempt to access the indent-level property using optional chaining is a syntax error:

const indent = options?.formatting?."indent-level";

We cannot use the ?. operator directly followed by a string literal — that would be invalid syntax. Instead, we can use the bracket notation of optional chaining and access the indent-level property using the ?.[] operator:

const indent = options?.formatting?.["indent-level"];

Here's our complete serializeJSON function:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.["indent-level"];
  return JSON.stringify(value, null, indent);
}

It's pretty much the same as before, aside from additional square brackets for the final property access.

The ?.() Operator: Method Calls

The third and final operator in the optional chaining family is ?.(). We can use the ?.() operator to invoke a method which may not exist.

To see when this operator is useful, let's change our SerializationOptions type once again. We'll replace the indent property (typed as a number) by a getIndent property (typed as a parameterless function returning a number):

type SerializationOptions = {
  formatting?: {
    getIndent?: () => number;
  };
};

We can call our serializeJSON function and specify an indentation level of two as follows:

const json = serializeJSON(user, {
  formatting: {
    getIndent: () => 2,
  },
});

To get the indentation level within our serializeJSON function, we can use the ?.() operator to conditionally invoke the getIndent method if (and only if) it is defined:

const indent = options?.formatting?.getIndent?.();

If the getIndent method is not defined, no attempt will be made to invoke it. The entire property chain will evaluate to undefined in that case, avoiding the infamous "getIndent is not a function" error.

Here's our complete serializeJSON function once again:

function serializeJSON(value: any, options?: SerializationOptions) {
  const indent = options?.formatting?.getIndent?.();
  return JSON.stringify(value, null, indent);
}
Compiling Optional Chaining to Older JavaScript

Now that we've seen how the optional chaining operators work and how they're type-checked, let's have a look at the compiled JavaScript which the TypeScript compiler emits when targeting older JavaScript versions.

Here's the JavaScript code that the TypeScript compiler will emit, with whitespace adjusted for readability:

function serializeJSON(value, options) {
  var _a, _b;
  var indent =
    (_b =
      (_a =
        options === null || options === void 0
          ? void 0
          : options.formatting) === null || _a === void 0
        ? void 0
        : _a.getIndent) === null || _b === void 0
      ? void 0
      : _b.call(_a);
  return JSON.stringify(value, null, indent);
}

There's quite a lot going on in the assignment to the indent variable. Let's simplify the code step by step. We'll start by renaming the local variables _a and _b to formatting and getIndent, respectively:

function serializeJSON(value, options) {
  var formatting, getIndent;
  var indent =
    (getIndent =
      (formatting =
        options === null || options === void 0
          ? void 0
          : options.formatting) === null || formatting === void 0
        ? void 0
        : formatting.getIndent) === null || getIndent === void 0
      ? void 0
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

Next, let's address the void 0 expression. The void operator always produces the value undefined, no matter what value it's applied to. We can replace the void 0 expression by the value undefined directly:

function serializeJSON(value, options) {
  var formatting, getIndent;
  var indent =
    (getIndent =
      (formatting =
        options === null || options === undefined
          ? undefined
          : options.formatting) === null || formatting === undefined
        ? undefined
        : formatting.getIndent) === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

Next, let's extract the assignment to the formatting variable into a separate statement:

function serializeJSON(value, options) {
  var formatting =
    options === null || options === undefined
      ? undefined
      : options.formatting;

  var getIndent;
  var indent =
    (getIndent =
      formatting === null || formatting === undefined
        ? undefined
        : formatting.getIndent) === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);
  return JSON.stringify(value, null, indent);
}

Let's do the same with the assignment to getIndent and add some whitespace:

function serializeJSON(value, options) {
  var formatting =
    options === null || options === undefined
      ? undefined
      : options.formatting;

  var getIndent =
    formatting === null || formatting === undefined
      ? undefined
      : formatting.getIndent;

  var indent =
    getIndent === null || getIndent === undefined
      ? undefined
      : getIndent.call(formatting);

  return JSON.stringify(value, null, indent);
}

Lastly, let's combine the checks using === for the values null and undefined into a single check using the == operator. Unless we're dealing with the special document.all value in our null checks, the two are equivalent:

function serializeJSON(value, options) {
  var formatting = options == null
    ? undefined
    : options.formatting;

  var getIndent = formatting == null
    ? undefined
    : formatting.getIndent;

  var indent = getIndent == null
    ? undefined
    : getIndent.call(formatting);

  return JSON.stringify(value, null, indent);
}

Now the structure of the code is a lot more apparent. You can see that TypeScript is emitting the null checks that we would have written ourselves if we hadn't been able to use the optional chaining operators.

urn:uuid:43da542e-0260-4930-aead-36d7209e8c1b
Extensions
How to Squash the First Two Commits in a Git Repository
Git 1.7.12 introduced the --root flag for the rebase command that lets you rewrite all the history leading to a specific commit down to the root commit.
Show full content

I recently needed to squash the first two commits in one of my Git repositories. As usual, I ran the git rebase -i command to do an interactive rebase, but I noticed that the root commit didn't appear in the list of commits.

Here's what my Git history looked like:

$ git log --graph --oneline
* fe2c946 (HEAD -> main) More changes
* 2702f8b Small tweaks
* ffb98dd Initial commit

When I ran git rebase -i ffb98dd, this was the output I got (omitted for brevity):

pick 2702f8b Small tweaks
pick fe2c946 More changes

# Rebase ffb98dd..fe2c946 onto ffb98dd (2 commands)
# ...

As you can see, the second commmit 2702f8b and the third commit fe2c946 were listed, but the initial commit ffb98dd wasn't. So how do you squash the second commit into the root commit if the root commit isn't listed?

The solution for this problem was introduced in Git 1.17.12. We can now specify the --root flag for the rebase command to rebase all reachable commits up to the root:

$ git rebase -i --root

This allows us to rewrite the Git history down to the root commit. Now, the output includes the root commit ffb98dd in the first line:

pick ffb98dd Initial commit
pick 2702f8b Small tweaks
pick fe2c946 More changes

# Rebase fe2c946 onto 6fafbe0 (3 commands)
# ...

We can use the squash command in the second line to combine ffb98dd and 2702f8b into a single commit:

pick ffb98dd Initial commit
squash 2702f8b Small tweaks
pick fe2c946 More changes

# Rebase fe2c946 onto 6fafbe0 (3 commands)
# ...

Now, we need to choose a message for the new combined commit. I kept "Initial commit" since it's still an accurate description. Once the command completes, the Git history looks like this:

* bfd9495 (HEAD -> main) More changes
* 34901ec Initial commit

And there we go! The changes made in the "Small tweaks" commit 2702f8b have been folded into our initial commit. Using the --root option with the rebase command, we were able to squash the first two commits into a single one.

urn:uuid:ecc53675-7a11-4ec1-b4e7-9668f67f60db
Extensions
Nullish Coalescing: The ?? Operator in TypeScript
TypeScript 3.7 added support for the ?? operator, which is known as the nullish coalescing operator. We can use this operator to provide a fallback value for a value that might be null or undefined.
Show full content

TypeScript 3.7 added support for the ?? operator, which is known as the nullish coalescing operator. We can use this operator to provide a fallback value for a value that might be null or undefined.

Truthy and Falsy Values in JavaScript

Before we dive into the ?? operator, let's recall that JavaScript values can either be truthy or falsy: when coerced to a Boolean, a value can either produce the value true or false. In JavaScript, the following values are considered to be falsy:

  • false
  • 0
  • -0
  • 0n
  • NaN
  • ""
  • null
  • undefined

All other JavaScript values will produce the value true when coerced to a Boolean and are thus considered truthy.

Providing Fallback Values with the ?? Operator

The ?? operator can be used to provide a fallback value in case another value is null or undefined. It takes two operands and is written like this:

value ?? fallbackValue;

If the left operand is null or undefined, the ?? expression evaluates to the right operand:

null ?? "n/a";
// "n/a"

undefined ?? "n/a";
// "n/a"

Otherwise, the ?? expression evaluates to the left operand:

false ?? true;
// false

0 ?? 100;
// 0

"" ?? "n/a";
// ""

NaN ?? 0;
// NaN

Notice that all left operands above are falsy values. If we had used the || operator instead of the ?? operator, all of these expressions would've evaluated to their respective right operands:

false || true;
// true

0 || 100;
// 100

"" || "n/a";
// "n/a"

NaN || 0;
// 0

This behavior is why you shouldn't use the || operator to provide a fallback value for a nullable value. For falsy values, the result might not be the one you wanted or expected. Consider this example:

type Options = {
  prettyPrint?: boolean;
};

function serializeJSON(value: unknown, options: Options): string {
  const prettyPrint = options.prettyPrint ?? true;
  // ...
}

The expression options.prettyPrint ?? true lets us provide the default value true in case that the prettyPrint property contains the value null or undefined. If prettyPrint contains the value false, the expression false ?? true still evaluates to false, which is exactly the behavior we want here.

Note that using the || operator here would lead to incorrect results. options.prettyPrint || true would evaluate to true for the values null and undefined, but also for the value false. This would clearly not be intended. I've seen this happen in practice a handful of times, so make sure to keep this case in mind and use towards the ?? operator instead.

Compiled Output: ES2020 and Newer

The nullish coalescing operator has reached Stage 4 ("Finished") of the TC39 process and is now officially part of ES2020. Therefore, the TypeScript compiler will emit the ?? operator as is without any downleveling when you're targeting "ES2020" (or a newer language version) or "ESNext" in your tsconfig.json file:

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2020"
  }
}

So, this simple expression will be emitted unchanged:

value ?? fallbackValue;

If you're planning on using the ?? operator while targeting "ES2020" or a newer language version, head over to caniuse.com and node.green and make sure that all the JavaScript engines you need to support have implemented the operator.

Compiled JavaScript Output: ES2019 and Older

If you're targeting "ES2019" or an older language version in your tsconfig.json file, the TypeScript compiler will rewrite the nullish coalescing operator into a conditional expression. That way, we can start using the ?? operator in our code today and still have the compiled code successfully parse and execute in older JavaScript engines.

Let's look at the same simple ?? expression again:

value ?? fallbackValue;

Assuming we're targeting "ES2019" or a lower language version, the TypeScript compiler will emit the following JavaScript code:

value !== null && value !== void 0 ? value : fallbackValue;

The value variable is compared against both null and undefined (the result of the expression void 0). If both comparisons produce the value false, the entire expression evaluates to value; otherwise, it evaluates to fallbackValue.

Now, let's look at a slightly more complex example. Instead of a simple value variable, we're going to use a getValue() call expression as the left operand of the ?? operator:

const value = getValue() ?? fallbackValue;

In this case, the compiler will emit the following JavaScript code (modulo whitespace differences):

var _a;
const value = (_a = getValue()) !== null && _a !== void 0
  ? _a
  : fallbackValue;

You can see that the compiler generated an intermediate variable _a to store the return value of the getValue() call. The _a variable is then compared against null and void 0 and (potentially) used as the resulting value of the entire expression. This intermediate variable is necessary so that the getValue function is only called once.

Compiled Output: Checking for null and undefined

You might be wondering why the compiler emits the following expression to check the value variable against null and undefined:

value !== null && value !== void 0;

Couldn't the compiler emit the following shorter check instead?

value != null;

Unfortunately, it can't do that without sacrificing correctness. For almost all values in JavaScript, the comparison value == null is equivalent to value === null || value === undefined. For those values, the negation value != null is equivalent to value !== null && value !== undefined. However, there is one value for which these two checks aren't equivalent, and that value is document.all:

document.all === null;
// false

document.all === undefined;
// false

document.all == null;
// true

document.all == undefined;
// true

The value document.all is not considered to be strictly equal to either null or undefined, but it is considered to be loosely equal to both null and undefined. Because of this anomaly, the TypeScript compiler can't emit value != null as a check because it would produce incorrect results for document.all.

You can read more about this curious behavior in an answer to the Why is document.all falsy? question on Stack Overflow. Oh, the things we do for web compatibility.

urn:uuid:6b7a17fe-9a6b-4749-863d-e21810336d24
Extensions
Declaring Global Variables in TypeScript
Different approaches for declaring a global variable in TypeScript.
Show full content

Every now and then, you might want to statically type a global variable in TypeScript. For example, in some of my web applications, I need to pass a few properties from my markup rendered on the server to my JavaScript code running in the browser. To do that, I typically define a global variable named __INITIAL_DATA__ within an inline script and assign to it a JSON-serialized object:

<script>
  window.__INITIAL_DATA__ = {
    userID: "536891193569405430",
  };
</script>

Now, if I try to access window.__INITIAL_DATA__ in a TypeScript file, the compiler will produce a type error because it can't find a definition of the __INITIAL_DATA__ property anywhere:

// Property '__INITIAL_DATA__' does not exist
// on type 'Window & typeof globalThis'
const initialData = window.__INITIAL_DATA__;

I'm going to show you a few different approaches for letting TypeScript know about the window.__INITIAL_DATA__ property and making the type error go away.

Using a Type Assertion

The quickest way to make the type error go away is to use the any type in a type assertion. We can treat the window object to be of type any so that we can access its __INITIAL_DATA__ property:

const initialData = (window as any).__INITIAL_DATA__;

This solution works, and we longer get a type error. This is a pragmatic approach if you need an ad-hoc way to access a property on the window object that TypeScript doesn't know about.

The (window as any).__INITIAL_DATA__ expression is of type any, and therefore initialData is of type any too. We could go one step further and use another type assertion to give the initialData variable a more specific type:

type InitialData = {
  userID: string;
};

const initialData = (window as any).__INITIAL_DATA__ as InitialData;

Now, we can access initialData.userID in a type-safe way:

const userID = initialData.userID; // Type string

Do keep in mind that this is not a guarantee that window.__INITIAL_DATA__ will be set correctly at runtime. The type checker trusts us and it is our job to make sure that we assign an object with the expected shape to window.__INITIAL_DATA__.

Declare a Global Variable

Another approach is to declare a global variable using the declare var syntax. This way, we can let TypeScript know that it can expect to find a global variable with the given name and type:

declare var __INITIAL_DATA__: InitialData;

We can now access the __INITIAL_DATA__ variable directly …

const initialData = __INITIAL_DATA__;

… or off of the window object:

const initialData = window.__INITIAL_DATA__;

Note that the access via window.__INITIAL_DATA__ will not work from within an ECMAScript module. If your JavaScript file contains top-level import or export declarations, it is considered a module, and you will receive a type error if you try to access the __INITIAL_DATA__ on the window object.

You can declare a global variable in the global scope by using the declare global { ... } syntax to be able to access both window.__INITIAL_DATA__ as well as __INITIAL_DATA__ directly within a JavaScript module.:

export function someExportedFunction() {
  // ...
}

declare global {
  var __INITIAL_DATA__: InitialData;
}

const initialData = window.__INITIAL_DATA__;

If you need to access window.__INITIAL_DATA__ in several files or modules, it might be a good idea to create a globals.d.ts file in your project. In that file, you can declare all global variables you'll use:

declare var __INITIAL_DATA__: InitialData;

As long as globals.d.ts is part of your TypeScript project, the compiler will know that __INITIAL_DATA__ is a global variable, and it will let you access it via both __INITIAL_DATA__ as well as window.__INITIAL_DATA__.

Augmenting the Window Interface

Lastly, you can use TypeScript's interface declaration merging to let the compiler know that it can expect to find a property named __INITIAL_DATA__ on the Window type and therefore the window object. To do that, you'll need to define an interface named Window with a property named __INITIAL_DATA__:

interface Window {
  __INITIAL_DATA__: InitialData;
}

TypeScript will merge this interface definition together with the Window interface defined in lib.dom.d.ts, resulting in a single Window type. Now, the following assignment will no longer produce a type error:

const initialData = window.__INITIAL_DATA__;

Note that once again, this approach will not work within a JavaScript module. You'll need to use the declare global { ... } syntax again in order for the window.__INITIAL_DATA__ expression to type-check correctly:

export function someExportedFunction() {
  // ...
}

declare global {
  interface Window {
    __INITIAL_DATA__: InitialData;
  }
}

const initialData = window.__INITIAL_DATA__;
urn:uuid:94d82e13-cdfe-4efe-ab22-20c6fbeef566
Extensions
Concatenating Arrays in JavaScript
In JavaScript, there are different approaches to concatenating multiple arrays into a single one. A comparison of push(), concat(), and spread syntax.
Show full content

It's a common task to concatenate multiple arrays into a single one. In JavaScript, there are several different approaches we can take. Some of them mutate the target array; others leave all input arrays unchanged and return a new array instead.

In this post, I want to compare the following common approaches:

  • Appending elements to an existing array with Array.prototype.push()
  • Appending elements to a new array with Array.prototype.push()
  • Concatenating multiple arrays with Array.prototype.concat()
  • Using spread syntax in an array literal

Let's take a look.

Appending Elements to an Existing Array with Array.prototype.push()

First up, the good old Array.prototype.push() method. Let's assume we have the following two arrays:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

We can append all elements of array2 to array1 by looping over array2 and calling array1.push() repeatedly:

for (const element of array2) {
  array1.push(element);
}

Once that code has run, array1 now contains all six values; it has been modified in place. array2, on the other hand, remains unchanged:

array1; // [1, 2, 3, 4, 5, 6]
array2; // [4, 5, 6]

Instead of the for...of loop, we could've passed all elements of array2 as arguments to the push() method call using spread syntax:

array1.push(...array2);

This is equivalent to the following method call:

array1.push(4, 5, 6);

The result is the same in all cases. array1 now contains all six values, array2 remains unchanged:

array1; // [1, 2, 3, 4, 5, 6]
array2; // [4, 5, 6]

Sometimes, mutating the target array might not be the desired behavior. This is particularly important when you're writing your code in a functional style, composing pure functions that don't have side effects. Those functions should not modify any of their parameters; you therefore shouldn't call the push() method on an array that was passed to the function as a parameter.

Appending Elements to a New Array with Array.prototype.push()

Let's take a look at an approach that uses the push() method without mutating any of the arrays that we want to concatenate. Here are our two input arrays again:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

Instead of appending the elements of array2 to array1, we can create a new empty array and push the elements of both array1 and array2 into that:

const concatenated = [];
concatenated.push(...array1);
concatenated.push(...array2);

Here's what the three arrays look like after the above code has finished executing:

array1; // [1, 2, 3]
array2; // [4, 5, 6]
concatenated; // [1, 2, 3, 4, 5, 6]

The push() is still a mutating method, but in this case, it only mutated concatenated, leaving array1 and array2 unchanged. This approach is fine even within a pure function that takes array1 and array2 as parameters. As long as the function is returning the same value when given the same parameters and it doesn't have any side effects (such as modifying its parameters), it is still considered a pure function, even if we locally use mutation within the function body.

Concatenating Multiple Arrays with Array.prototype.concat()

Now that we've seen how to work with the Array.prototype.push() method, which mutates the target array, let's take a look at Array.prototype.concat(), a non-mutating method. We'll start out with the same two arrays again:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

We'll then call the concat() method on array1, passing array2 as a parameter:

const concatenated = array1.concat(array2);

Since the concat() method is non-mutating, it neither modifies array1 nor array2. Instead, it returns a new array that contains all elements of array1 and array2 concatenated together:

array1; // [1, 2, 3]
array2; // [4, 5, 6]
concatenated; // [1, 2, 3, 4, 5, 6]

Just like the push() method, the concat() method accepts arbitrarily many arguments. That's useful if you want to concatenate three or more arrays together:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const array3 = [7, 8, 9];
const concatenated = array1.concat(array2, array3);

And just like before, all input arrays remain unchanged:

array1; // [1, 2, 3]
array2; // [4, 5, 6]
array3; // [7, 8, 9]
concatenated; // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Sometimes, you might not know upfront how many arrays you want to concatenate. Let's say we have an array of arrays that we want to concatenate into a single one:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const array3 = [7, 8, 9];
const arrays = [array1, array2, array3];

Using spread syntax again, we can spread all elements of arrays as arguments into the concat() method call:

const concatenated = [].concat(...arrays);

Notice that we're creating an empty array here so that we can call the concat() method on it. Since it doesn't contain any elements, the empty array doesn't change the resulting concatenated array. And as before, all input arrays remain unchanged:

array1; // [1, 2, 3]
array2; // [4, 5, 6]
array3; // [7, 8, 9]
arrays; // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
concatenated; // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Note that the concat() method doesn't recursively flatten arrays. It concatenates all elements in all of its arrays without unwrapping nested arrays:

const array1 = [1, [2], 3];
const array2 = [4, [5, [6]]];
const concatenated = array1.concat(array2);

The resulting concatenated array contains the three elements of array1, followed by the two elements of array2, totaling five elements:

concatenated; // [1, [2], 3, 4, [5, [6]]]
concatenated.length; // 5
Using Spread Syntax in an Array Literal

Lastly, let's look at spread syntax in array literals. Just like before, we'll assume we have two input arrays that we want to concatenate:

const array1 = [1, 2, 3];
const array2 = [4, 5, 6];

Using spread syntax in an array literal, we can create a new array that contains all elements of array1, followed by all elements of array2:

const concatenated = [...array1, ...array2];

And once again, we can see that neither array1 nor array2 has been modified:

array1; // [1, 2, 3]
array2; // [4, 5, 6]
concatenated; // [1, 2, 3, 4, 5, 6]

The great thing about spread syntax is that it invokes the iteration protocol of the element that we're spreading. This means that spreading works with any iterable, rather than only with arrays. For example, we could spread all values in a Set into a new array:

const uniques = new Set([1, 2, 2, 3, 3, 3]);
const array = [...uniques];

uniques; // Set (3) {1, 2, 3}
array; // [1, 2, 3]

This is useful when you want to concatenate multiple arrays into a single one and remove any duplicate values:

const array1 = [1, 2, 3];
const array2 = [2, 3, 4];
const uniques = [...new Set([...array1, ...array2])];

We're concatenating array1 and array2 into a new array that contains all six elements: [1, 2, 3, 2, 3, 4]. That new array is passed to the Set constructor. Set can't contain duplicate values, so when we spread the set into the outer new array, we end up with four unique values:

array1; // [1, 2, 3]
array2; // [2, 3, 4]
uniques; // [1, 2, 3, 4]
Summary

We've seen different approaches to concatenate multiple arrays into a single one:

  • Using the Array.prototype.push() method
  • Using the Array.prototype.concat() method
  • Using spread syntax in array literals

Most importantly, you should remember that the push() mutates the target array, modifying it in place. The concat() method and spread syntax in array literals, on the other hand, are non-mutating; both approaches create a new array instead.

urn:uuid:e3c28813-f8e0-4f3b-a6a5-41251f66f984
Extensions
The Omit Helper Type in TypeScript
TypeScript 3.5 added an Omit helper type which lets us create an object type that omits specific properties from another object type.
Show full content

In version 3.5, TypeScript added an Omit<T, K> helper type to the lib.es5.d.ts type definition file that ships as part of the TypeScript compiler. The Omit<T, K> type lets us create an object type that omits specific properties from another object type:

type User = {
  id: string;
  name: string;
  email: string;
};

type UserWithoutEmail = Omit<User, "email">;

// This is equivalent to:
type UserWithoutEmail = {
  id: string;
  name: string;
};

The Omit<T, K> helper type is defined in lib.es5.d.ts like this:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

To untangle this type definition and understand how it works, let's see how we could've come up with our own version of the Omit<T, K> helper type ourselves.

Defining the Omit<T, K> Helper Type

Let's start with the same User type we've seen above:

type User = {
  id: string;
  name: string;
  email: string;
};

First, we need to be able to retrieve all keys of the User type. We can use the keyof operator to retrieve a union of string literal types that contains all property keys of this object type:

type UserKeys = keyof User;

// This is equivalent to:
type UserKeys = "id" | "name" | "email";

Next, we need to be able to exclude a specific string literal type from a union of string literal types. In the case of our User type, we want to exclude the type "email" from the union "id" | "name" | "email". We can use the Exclude<T, U> helper type to do that:

type UserKeysWithoutEmail = Exclude<UserKeys, "email">;

// This is equivalent to:
type UserKeysWithoutEmail = Exclude<"id" | "name" | "email", "email">;

// This is equivalent to:
type UserKeysWithoutEmail = "id" | "name";

The Exclude<T, U> type is defined in lib.es5.d.ts like this:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

It's using a conditional type and the never type. Using the Exclude<T, U> helper type, we're removing those types in our union type "id" | "name" | "email" that are assignable to the "email" type. That is only true for the string literal type "email" itself, so we're left with the union type "id | "name".

Finally, we need to create an object type that contains a subset of the properties of our User type. Specifically, we want to create an object type that contains only those properties whose keys are found in the UserKeysWithoutEmail union type. We can use the Pick<T, K> helper type to pick those properties off of our User type:

type UserWithoutEmail = Pick<User, UserKeysWithoutEmail>;

// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name">;

// This is equivalent to:
type UserWithoutEmail = {
  id: string;
  name: string;
};

Here's how the Pick<T, K> helper type is defined within lib.es5.d.ts:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

The Pick<T, K> type is a mapped type that's using the keyof operator and an indexed access type T[P] to retrieve the type of the property P in the object type T.

Now, let's summarize all the type operations we've performed using keyof, Exclude<T, U>, and Pick<T, K> in a single type:

type UserWithoutEmail = Pick<User, Exclude<keyof User, "email">>;

Notice that this type is specific to our User type. Let's make this a generic type so we can reuse it in other places:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

We can now use this type to compute our UserWithoutEmail type:

type UserWithoutEmail = Omit<User, "email">;

Since object keys can only be strings, numbers, or symbols, we can add a generic constraint to the type parameter K of our Omit<T, K> helper type to only allow types string, number, or symbol for keys:

type Omit<T, K extends string | number | symbol> = Pick<T, Exclude<keyof T, K>>;

The generic constraint extends string | number | symbol is a bit verbose. We can replace the string | number | symbol union type by the keyof any type since the two are equivalent:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

And there we go! We've arrived at the exact definition of the Omit<T, K> helper type as it is found within the lib.es5.d.ts type definition file:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Unrolling Omit<User, "email">

Here's a step-by-step evaluation of the Omit<User, "email"> type. Try to follow every step to understand how TypeScript is computing the final type:

type User = {
  id: string;
  name: string;
  email: string;
};

type UserWithoutEmail = Omit<User, "email">;

// This is equivalent to:
type UserWithoutEmail = Pick<User, Exclude<keyof User, "email">>;

// This is equivalent to:
type UserWithoutEmail = Pick<User, Exclude<"id" | "name" | "email", "email">>;

// This is equivalent to:
type UserWithoutEmail = Pick<
  User,
  | ("id" extends "email" ? never : "id")
  | ("name" extends "email" ? never : "name")
  | ("email" extends "email" ? never : "email")
>;

// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name" | never>;

// This is equivalent to:
type UserWithoutEmail = Pick<User, "id" | "name">;

// This is equivalent to:
type UserWithoutEmail = {
  [P in "id" | "name"]: User[P];
};

// This is equivalent to:
type UserWithoutEmail = {
  id: User["id"];
  name: User["name"];
};

// This is equivalent to:
type UserWithoutEmail = {
  id: string;
  name: string;
};

Et voilà, our final UserWithoutEmail type.

urn:uuid:11a262db-bb6c-42dc-83db-03bcf97b23ec
Extensions
Fast Searching with ripgrep
In this post, I want to introduce you to ripgrep, a smart and fast command line search tool that I find myself using all the time when programming.
Show full content

In this post, I want to introduce you to ripgrep, a smart and fast command line search tool that I find myself using all the time when programming. ripgrep recursively searches directories for a regex pattern and outputs all matches that it finds.

Why ripgrep?

So what makes ripgrep so great? After all, there are plenty of other search tools out there already, like grep, ack, or The Silver Searcher. For me, it boils down to the following reasons:

  • ripgrep is smart. It picks sensible defaults out of the box. I like that! For example, ripgrep respects .gitignore files and skips matching files and directories by default. It also ignores binary files, skips hidden files and directories, and doesn't follow symbolic links.
  • ripgrep is fast. In fact, it's very fast. I've thrown hundreds of thousands of files at it and didn't encounter any performance issues. Check out ripgrep is faster than {grep, ag, git grep, ucg, pt, sift} for a detailed analysis and various performance benchmarks.

ripgrep also has full Unicode support, can search compressed files, and optionally lets you switch its regex engine to use PCRE2 regular expressions.

Installation

If you're using Homebrew, you can run the following command to install ripgrep:

$ brew install ripgrep

If you're using a different package manager, you can find a comprehensive list of installation instructions in the README.md on GitHub.

The Basics

The name of the ripgrep executable is rg. In its most basic form, a simple search can look like this:

$ rg '// TODO'

This command will recursively search all files in the current directory (and its subdirectories) for the string // TODO and output the matches that it finds. If I run this command within the src directory of the prettier repository, the output looks like this:

$ rg '// TODO'
language-css/parser-postcss.js
521:  // TODO: Remove this hack when this issue is fixed:

language-markdown/parser-markdown.js
121:    // TODO: Delete this in 2.0

language-handlebars/parser-glimmer.js
32:      // TODO: `locStart` and `locEnd` should return a number offset

common/util-shared.js
42:  mapDoc, // TODO: remove in 2.0, we already exposed it in docUtils

language-js/utils.js
239:// TODO: This is a bad hack and we need a better way to distinguish between

language-html/utils.js
80:  // TODO: handle non-text children in <pre>

common/internal-plugins.js
91:      // TODO: switch these to just `postcss` and use `language` instead.
134:      // TODO: Delete this in 2.0

language-html/constants.evaluate.js
21:  // TODO: send PR to upstream

language-js/printer-estree.js
5:// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.

The matches are grouped by file name. For each match, ripgrep prints the line number and highlights the matching substring.

Frequently Used Options

For the remainder of this article, I'll go over several ripgrep options that I find myself using frequently to perform various search tasks when programming. I'm using the prettier repository to demonstrate the different options and what effect they have.

Feel free to clone the repository and follow along:

$ git clone https://github.com/prettier/prettier.git
$ cd prettier

Also, unless stated otherwise, I'm running all commands from within the src directory:

$ cd src
No Options

Let's start with running ripgrep without any options. The default behavior might do exactly what you want already. Here, I am searching for the string // TODO within the current working directory:

$ rg '// TODO'
language-css/parser-postcss.js
521:  // TODO: Remove this hack when this issue is fixed:

language-markdown/parser-markdown.js
121:    // TODO: Delete this in 2.0

language-handlebars/parser-glimmer.js
32:      // TODO: `locStart` and `locEnd` should return a number offset

common/util-shared.js
42:  mapDoc, // TODO: remove in 2.0, we already exposed it in docUtils

language-js/utils.js
239:// TODO: This is a bad hack and we need a better way to distinguish between

language-html/utils.js
80:  // TODO: handle non-text children in <pre>

common/internal-plugins.js
91:      // TODO: switch these to just `postcss` and use `language` instead.
134:      // TODO: Delete this in 2.0

language-html/constants.evaluate.js
21:  // TODO: send PR to upstream

language-js/printer-estree.js
5:// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.

We can see all matches, grouped by file name, with line numbers and highlighted matching substrings. If you want to quickly find a given string in a bunch of files and directories, this could be sufficient already.

Files with Matches

Sometimes, you're not interested in seeing the matches themselves, but rather the paths of all files that contain at least one match. You can use the --files-with-matches option for that, or -l for short:

$ rg -l '// TODO'
language-markdown/parser-markdown.js
common/util-shared.js
language-html/constants.evaluate.js
language-css/parser-postcss.js
common/internal-plugins.js
language-js/printer-estree.js
language-html/utils.js
language-js/utils.js
language-handlebars/parser-glimmer.js

Note that ripgrep doesn't emit the files in a specific sort order by default. This is for performance reasons. If you want the list of file paths to be sorted alphabetically, you can use the --sort path option:

$ rg -l '// TODO' --sort path
common/internal-plugins.js
common/util-shared.js
language-css/parser-postcss.js
language-handlebars/parser-glimmer.js
language-html/constants.evaluate.js
language-html/utils.js
language-js/printer-estree.js
language-js/utils.js
language-markdown/parser-markdown.js

Please be aware that using the --sort path option disables all parallelism in ripgrep. Unless you're searching a large number of files though, you likely won't notice much of a performance difference.

The -l flag is particularly useful to pipe ripgrep's output into another program and perform additional operations on the matching files. For example, you could use ripgrep to find all files matching the string @format and format them with Prettier using the prettier executable:

$ rg -l '@format' | xargs prettier --write
Files Without a Match

Sometimes, you might not be interested in files that do contain a match, but rather in those that don't. The --files-without-match option outputs exactly those files. Unlike the --files-with-matches option, the --files-without-match option does not have a short alias.

The following command lists all files that don't contain any of the strings var, let, or const. These JavaScript files don't contain any local variable declarations:

$ rg --files-without-match '\b(var|let|const)\b'
language-yaml/pragma.js
language-graphql/pragma.js
document/index.js
utils/get-last.js
language-js/preprocess.js
common/internal-plugins.js
common/third-party.js
utils/arrayify.js
language-html/pragma.js
common/errors.js
language-html/clean.js

And again, we can sort the list of files by using the --sort path option:

$ rg --files-without-match '\b(var|let|const)\b' --sort path
common/errors.js
common/internal-plugins.js
common/third-party.js
document/index.js
language-graphql/pragma.js
language-html/clean.js
language-html/pragma.js
language-js/preprocess.js
language-yaml/pragma.js
utils/arrayify.js
utils/get-last.js

Notice that we're using several regular expression features in our search pattern:

  • \b matches a word boundary. That way, the string delete won't match the let pattern.
  • | denotes an alternation. The pattern var|let|const matches any string that matches any of the patterns var, let, or const.

ripgrep will treat the search pattern as a regular expression by default; there's no need to specify another flag to turn the search pattern into a regular expression.

Inverting Matches

Sometimes, you might be interested in all lines that do not match a given pattern, rather than those lines that do. ripgrep lets us show those lines using the --invert-match (or -v for short) flag.

Every now and then, I want to run a sanity check on all lines of code I've changed in a given Git commit. This is particularly helpful when running a codemod that changes hundreds or thousands of files. In those cases, I want to see a sorted and de-duplicated list of all changed lines. Here's the command I use:

git show | rg '^[-+]' | rg -v '^[-+]{3}' | sort | uniq

For the commit 6daa7e199e2d71cee66f5ebee3b2efe4648d7b99 in the Prettier repository, this is the output:

+      - "patch-release"
-      - patch-release

If I were to remove the rg -v '^[-+]{3}' bit from the pipe, the output would include file names as well, which is not what I want:

+      - "patch-release"
+++ b/.github/workflows/dev-test.yml
+++ b/.github/workflows/lint.yml
+++ b/.github/workflows/prod-test.yml
-      - patch-release
--- a/.github/workflows/dev-test.yml
--- a/.github/workflows/lint.yml
--- a/.github/workflows/prod-test.yml

By piping the output of the first search through rg -v '^[-+]{3}', I'm excluding all lines that start with three pluses or minuses, giving me a cleaner output at the end.

Fixed Strings

Usually, it's useful that ripgrep treats every search pattern as a regular expression by default. We've seen in the previous section how we can search for several strings using the pattern var|let|const using an alternation, and there was no need for an additional flag to tell ripgrep to interpret the pattern as a regular expression rather than a fixed string.

However, if we want to search for a string that is not a well-formed regular expression, we get an error:

$ rg '?.'
regex parse error:
    ?.
    ^
error: repetition operator missing expression

In the above example, our search for the pattern ?. failed because the pattern is malformed. In a regular expression, the ? character denotes a repetition operator that makes the previous expression optional. It must follow an expression, which it doesn't do here.

We can tell ripgrep that we want it to interpret the search string as a fixed string rather than a regular expression pattern. All characters that would have special meaning in a regular expression (e.g. $, ?, |, …) will be matched verbatim. The flag we need to use to turn on this behavior is called --fixed-strings, or -F for short:

$ rg -F '?.'
language-js/printer-estree.js
4763:    return "?.";

Now, the search has succeeded and we get all results matching the string ?. verbatim.

Context Around a Match

Sometimes, seeing only the matching lines themselves without any preceding or following lines might lack context. Take the search for // TODO as an example again:

$ rg '// TODO'
language-css/parser-postcss.js
521:  // TODO: Remove this hack when this issue is fixed:

common/util-shared.js
42:  mapDoc, // TODO: remove in 2.0, we already exposed it in docUtils

common/internal-plugins.js
91:      // TODO: switch these to just `postcss` and use `language` instead.
134:      // TODO: Delete this in 2.0

language-markdown/parser-markdown.js
121:    // TODO: Delete this in 2.0

language-handlebars/parser-glimmer.js
32:      // TODO: `locStart` and `locEnd` should return a number offset

language-js/utils.js
239:// TODO: This is a bad hack and we need a better way to distinguish between

language-js/printer-estree.js
5:// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.

language-html/constants.evaluate.js
21:  // TODO: send PR to upstream

language-html/utils.js
80:  // TODO: handle non-text children in <pre>

Wouldn't it be helpful if we could see a few lines following each // TODO comment to get an idea of the code to which each comment refers? It turns out that ripgrep can do that. We can specify the --context option (or -C for short) and pass it an argument N to have ripgrep display N lines before and after each matching line:

$ rg '// TODO' -C 2
language-css/parser-postcss.js
519-  }
520-
521:  // TODO: Remove this hack when this issue is fixed:
522-  // https://github.com/shellscape/postcss-less/issues/88
523-  const LessParser = require("postcss-less/dist/less-parser");

language-markdown/parser-markdown.js
119-  parsers: {
120-    remark: markdownParser,
121:    // TODO: Delete this in 2.0
122-    markdown: markdownParser,
123-    mdx: mdxParser

common/util-shared.js
40-  isPreviousLineEmpty,
41-  getNextNonSpaceNonCommentCharacterIndex,
42:  mapDoc, // TODO: remove in 2.0, we already exposed it in docUtils
43-  makeString: util.makeString,
44-  addLeadingComment: util.addLeadingComment,

common/internal-plugins.js
89-  {
90-    parsers: {
91:      // TODO: switch these to just `postcss` and use `language` instead.
92-      get css() {
93-        return eval("require")("../language-css/parser-postcss").parsers.css;
--
132-          .remark;
133-      },
134:      // TODO: Delete this in 2.0
135-      get markdown() {
136-        return eval("require")("../language-markdown/parser-markdown").parsers

language-js/utils.js
237-}
238-
239:// TODO: This is a bad hack and we need a better way to distinguish between
240-// arrow functions and otherwise
241-function isFunctionNotation(node, options) {

language-handlebars/parser-glimmer.js
30-      parse,
31-      astFormat: "glimmer",
32:      // TODO: `locStart` and `locEnd` should return a number offset
33-      // https://prettier.io/docs/en/plugins.html#parsers
34-      // but we need access to the original text to use

language-html/constants.evaluate.js
19-
20-const CSS_DISPLAY_TAGS = Object.assign({}, getCssStyleTags("display"), {
21:  // TODO: send PR to upstream
22-  button: "inline-block",
23-

language-html/utils.js
78-  }
79-
80:  // TODO: handle non-text children in <pre>
81-  if (
82-    isPreLikeNode(node) &&

language-js/printer-estree.js
3-const assert = require("assert");
4-
5:// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.
6-const comments = require("../main/comments");
7-const {

Now, we can see two lines before and after each // TODO comment, giving us some more context without having to open those files.

If you want to control the number of lines before and after the matching line independently, you can use the --before-context and --after-context options, respectively, or -B and -A for short. For example, here are all // TODO comments, followed by the next three lines:

$ rg '// TODO' -A 3
language-markdown/parser-markdown.js
121:    // TODO: Delete this in 2.0
122-    markdown: markdownParser,
123-    mdx: mdxParser
124-  }

common/util-shared.js
42:  mapDoc, // TODO: remove in 2.0, we already exposed it in docUtils
43-  makeString: util.makeString,
44-  addLeadingComment: util.addLeadingComment,
45-  addDanglingComment: util.addDanglingComment,

common/internal-plugins.js
91:      // TODO: switch these to just `postcss` and use `language` instead.
92-      get css() {
93-        return eval("require")("../language-css/parser-postcss").parsers.css;
94-      },
--
134:      // TODO: Delete this in 2.0
135-      get markdown() {
136-        return eval("require")("../language-markdown/parser-markdown").parsers
137-          .remark;

language-handlebars/parser-glimmer.js
32:      // TODO: `locStart` and `locEnd` should return a number offset
33-      // https://prettier.io/docs/en/plugins.html#parsers
34-      // but we need access to the original text to use
35-      // `loc.start` and `loc.end` objects to calculate the offset

language-js/utils.js
239:// TODO: This is a bad hack and we need a better way to distinguish between
240-// arrow functions and otherwise
241-function isFunctionNotation(node, options) {
242-  return isGetterOrSetter(node) || sameLocStart(node, node.value, options);

language-js/printer-estree.js
5:// TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.
6-const comments = require("../main/comments");
7-const {
8-  getParentExportDeclaration,

language-css/parser-postcss.js
521:  // TODO: Remove this hack when this issue is fixed:
522-  // https://github.com/shellscape/postcss-less/issues/88
523-  const LessParser = require("postcss-less/dist/less-parser");
524-  LessParser.prototype.atrule = function() {

language-html/constants.evaluate.js
21:  // TODO: send PR to upstream
22-  button: "inline-block",
23-
24-  // special cases for some css display=none elements

language-html/utils.js
80:  // TODO: handle non-text children in <pre>
81-  if (
82-    isPreLikeNode(node) &&
83-    node.children.some(
Specific File Types Only

The --type option, or -t for short, lets you restrict searches to files of a specific type. To see how this option works, let's move up one level from the src directory into the root of the prettier repository:

$ cd ..

Let's confirm the current working directory:

$ pwd
/Users/marius/code/prettier

Alright, now we're ready. We can run a search for @format only on JavaScript files:

$ rg -t js '@format'
src/language-yaml/pragma.js
12:  return `# @format\n\n${text}`;

src/language-graphql/pragma.js
8:  return "# @format\n\n" + text;

src/language-css/clean.js
35:     * @format

src/language-html/pragma.js
8:  return "<!-- @format -->\n\n" + text.replace(/^\s*\n/, "");

src/main/core-options.js
110:    description: "Insert @format pragma into file's first docblock comment.",
234:      Require either '@prettier' or '@format' to be present in the file's first docblock comment

tests/insert-pragma/js/module-with-pragma.js
5: * @format

tests/require-pragma/js/module-with-pragma.js
3: * @format

Or we could search within Markdown files only:

$ rg -t md '@format'
docs/cli.md
101:Valid pragmas are `@prettier` and `@format`.
105:Insert a `@format` pragma to the top of formatted files when pragma is absent. Works well when used in tandem with `--require-pragma`.

docs/options.md
258: * @format
270:Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with prettier. This works well when used in tandem with the `--require-pragma` option. If there is already a docblock at the top of the file then this option will add a newline to it with the @format marker.

website/blog/2017-09-15-1.7.0.md
108: * @format
187:- [**Add option to require @prettier or @format pragma**](https://github.com/prettier/prettier/pull/2772) by [@wbinnssmith](https://github.com/wbinnssmith)

website/blog/2017-05-03-1.3.0.md
25:- When pretty-printing a file, add `@format` to the first block comment like `@flow`.
26:- Have a lint rule with autofix that checks if the file is correctly pretty printed when `@format` is present.
29:- Update the default code templates to add `@format` to the header.
30:- When you run code formatting via cmd-shift-c inside of Nuclide, automatically insert the `@format` header.
31:- Disable all the stylistic rules like max-len when `@format` is in the header.
34:- When pushing a new release of prettier, also run it through all the files with `@format` in order to avoid getting warnings afterwards.
35:- Add tracking for the number of files with `@format` over time.

website/blog/2017-11-07-1.8.0.md
136:#### Add option to insert `@format` to first docblock if absent ([#2865](https://github.com/prettier/prettier/pull/2865)) by [@samouri](https://github.com/samouri)
138:In 1.7, we added an option called `--require-pragma` to require files contain an `/** @format */` pragma to be formatted. In order to add this pragma to a large set of files you can now use [`--insert-pragma`](https://prettier.io/docs/en/cli.html#insert-pragma) flag.

website/blog/2018-02-26-1.11.0.md
814: * @format
820: * @format

website/versioned_docs/version-stable/cli.md
102:Valid pragmas are `@prettier` and `@format`.
106:Insert a `@format` pragma to the top of formatted files when pragma is absent. Works well when used in tandem with `--require-pragma`.

website/versioned_docs/version-stable/options.md
259: * @format
271:Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with prettier. This works well when used in tandem with the `--require-pragma` option. If there is already a docblock at the top of the file then this option will add a newline to it with the @format marker.

tests/markdown/real-world-case.md
292:Valid pragmas are `@prettier` and `@format`.
695: * @format

tests/require-pragma/markdown/with-pragma-in-multiline.md
6:  @format

Note that the type specifiers js and md are not file name extensions themselves. The type specifiers represent a set of file name extensions that are considered to be of that type:

  • js represents the extensions *.js, *.jsx, and *.vue
  • md represents the extensions *.markdown, *.md, *.mdown, and *.mkdn

You can pull up the complete list of supported type specifiers and the corresponding file name extensions by running the rg --type-list command.

Using a Glob

Sometimes, using the --type (or -t for short) option might not give you enough control over which files to include in the search. In those cases, you can use the --glob (or -g for short) option. ripgrep will only search files whose paths match the specified glob.

For example, you could run a search for // TODO comments within only those JavaScript files whose name starts with "parser-":

$ rg -g 'parser-*.js' '// TODO'
language-markdown/parser-markdown.js
121:    // TODO: Delete this in 2.0

language-handlebars/parser-glimmer.js
32:      // TODO: `locStart` and `locEnd` should return a number offset

language-css/parser-postcss.js
521:  // TODO: Remove this hack when this issue is fixed:
Showing the Help Page

Lastly, if you ever forget what a specific option is called, or if you want to see what other options are available, ripgrep provides two different levels of help:

  • rg -h: short descriptions with a condensed layout
  • rg --help: long descriptions with detailed explanations

Here's what ripgrep 12.0.0 prints when running the rg -h command:

ripgrep 12.0.0
Andrew Gallant <jamslam@gmail.com>

ripgrep (rg) recursively searches your current directory for a regex pattern.
By default, ripgrep will respect your .gitignore and automatically skip hidden
files/directories and binary files.

ripgrep's default regex engine uses finite automata and guarantees linear
time searching. Because of this, features like backreferences and arbitrary
look-around are not supported. However, if ripgrep is built with PCRE2, then
the --pcre2 flag can be used to enable backreferences and look-around.

ripgrep supports configuration files. Set RIPGREP_CONFIG_PATH to a
configuration file. The file can specify one shell argument per line. Lines
starting with '#' are ignored. For more details, see the man page or the
README.

ripgrep will automatically detect if stdin exists and search stdin for a regex
pattern, e.g. 'ls | rg foo'. In some environments, stdin may exist when it
shouldn't. To turn off stdin detection explicitly specify the directory to
search, e.g. 'rg foo ./'.

Tip: to disable all smart filtering and make ripgrep behave a bit more like
classical grep, use 'rg -uuu'.

Project home page: https://github.com/BurntSushi/ripgrep

Use -h for short descriptions and --help for more details.

USAGE:
    rg [OPTIONS] PATTERN [PATH ...]
    rg [OPTIONS] [-e PATTERN ...] [-f PATTERNFILE ...] [PATH ...]
    rg [OPTIONS] --files [PATH ...]
    rg [OPTIONS] --type-list
    command | rg [OPTIONS] PATTERN

ARGS:
    <PATTERN>    A regular expression used for searching.
    <PATH>...    A file or directory to search.

OPTIONS:
    -A, --after-context <NUM>               Show NUM lines after each match.
        --auto-hybrid-regex                 Dynamically use PCRE2 if necessary.
    -B, --before-context <NUM>              Show NUM lines before each match.
        --binary                            Search binary files.
        --block-buffered                    Force block buffering.
    -b, --byte-offset                       Print the 0-based byte offset for each matching line.
    -s, --case-sensitive                    Search case sensitively (default).
        --color <WHEN>                      Controls when to use color.
        --colors <COLOR_SPEC>...            Configure color settings and styles.
        --column                            Show column numbers.
    -C, --context <NUM>                     Show NUM lines before and after each match.
        --context-separator <SEPARATOR>     Set the context separator string.
    -c, --count                             Only show the count of matching lines for each file.
        --count-matches                     Only show the count of individual matches for each file.
        --crlf                              Support CRLF line terminators (useful on Windows).
        --debug                             Show debug messages.
        --dfa-size-limit <NUM+SUFFIX?>      The upper size limit of the regex DFA.
    -E, --encoding <ENCODING>               Specify the text encoding of files to search.
        --engine <ENGINE>                   Specify which regexp engine to use. [default: default]
    -f, --file <PATTERNFILE>...             Search for patterns from the given file.
        --files                             Print each file that would be searched.
    -l, --files-with-matches                Only print the paths with at least one match.
        --files-without-match               Only print the paths that contain zero matches.
    -F, --fixed-strings                     Treat the pattern as a literal string.
    -L, --follow                            Follow symbolic links.
    -g, --glob <GLOB>...                    Include or exclude files.
        --glob-case-insensitive             Process all glob patterns case insensitively.
    -h, --help                              Prints help information. Use --help for more details.
        --heading                           Print matches grouped by each file.
        --hidden                            Search hidden files and directories.
        --iglob <GLOB>...                   Include or exclude files case insensitively.
    -i, --ignore-case                       Case insensitive search.
        --ignore-file <PATH>...             Specify additional ignore files.
        --ignore-file-case-insensitive      Process ignore files case insensitively.
        --include-zero                      Include files with zero matches in summary
    -v, --invert-match                      Invert matching.
        --json                              Show search results in a JSON Lines format.
        --line-buffered                     Force line buffering.
    -n, --line-number                       Show line numbers.
    -x, --line-regexp                       Only show matches surrounded by line boundaries.
    -M, --max-columns <NUM>                 Don't print lines longer than this limit.
        --max-columns-preview               Print a preview for lines exceeding the limit.
    -m, --max-count <NUM>                   Limit the number of matches.
        --max-depth <NUM>                   Descend at most NUM directories.
        --max-filesize <NUM+SUFFIX?>        Ignore files larger than NUM in size.
        --mmap                              Search using memory maps when possible.
    -U, --multiline                         Enable matching across multiple lines.
        --multiline-dotall                  Make '.' match new lines when multiline is enabled.
        --no-config                         Never read configuration files.
    -I, --no-filename                       Never print the file path with the matched lines.
        --no-heading                        Don't group matches by each file.
        --no-ignore                         Don't respect ignore files.
        --no-ignore-dot                     Don't respect .ignore files.
        --no-ignore-exclude                 Don't respect local exclusion files.
        --no-ignore-files                   Don't respect --ignore-file arguments.
        --no-ignore-global                  Don't respect global ignore files.
        --no-ignore-messages                Suppress gitignore parse error messages.
        --no-ignore-parent                  Don't respect ignore files in parent directories.
        --no-ignore-vcs                     Don't respect VCS ignore files.
    -N, --no-line-number                    Suppress line numbers.
        --no-messages                       Suppress some error messages.
        --no-mmap                           Never use memory maps.
        --no-pcre2-unicode                  Disable Unicode mode for PCRE2 matching.
        --no-require-git                    Do not require a git repository to use gitignores.
        --no-unicode                        Disable Unicode mode.
    -0, --null                              Print a NUL byte after file paths.
        --null-data                         Use NUL as a line terminator instead of \n.
        --one-file-system                   Do not descend into directories on other file systems.
    -o, --only-matching                     Print only matches parts of a line.
        --passthru                          Print both matching and non-matching lines.
        --path-separator <SEPARATOR>        Set the path separator.
    -P, --pcre2                             Enable PCRE2 matching.
        --pcre2-version                     Print the version of PCRE2 that ripgrep uses.
        --pre <COMMAND>                     search outputs of COMMAND FILE for each FILE
        --pre-glob <GLOB>...                Include or exclude files from a preprocessing command.
    -p, --pretty                            Alias for --color always --heading --line-number.
    -q, --quiet                             Do not print anything to stdout.
        --regex-size-limit <NUM+SUFFIX?>    The upper size limit of the compiled regex.
    -e, --regexp <PATTERN>...               A pattern to search for.
    -r, --replace <REPLACEMENT_TEXT>        Replace matches with the given text.
    -z, --search-zip                        Search in compressed files.
    -S, --smart-case                        Smart case search.
        --sort <SORTBY>                     Sort results in ascending order. Implies --threads=1.
        --sortr <SORTBY>                    Sort results in descending order. Implies --threads=1.
        --stats                             Print statistics about this ripgrep search.
    -a, --text                              Search binary files as if they were text.
    -j, --threads <NUM>                     The approximate number of threads to use.
        --trim                              Trim prefixed whitespace from matches.
    -t, --type <TYPE>...                    Only search files matching TYPE.
        --type-add <TYPE_SPEC>...           Add a new glob for a file type.
        --type-clear <TYPE>...              Clear globs for a file type.
        --type-list                         Show all supported file types.
    -T, --type-not <TYPE>...                Do not search files matching TYPE.
    -u, --unrestricted                      Reduce the level of "smart" searching.
    -V, --version                           Prints version information
        --vimgrep                           Show results in vim compatible format.
    -H, --with-filename                     Print the file path with the matched lines.
    -w, --word-regexp                       Only show matches surrounded by word boundaries.
urn:uuid:11a77245-32cc-4cb5-a67b-1bb229b3ec05
Extensions
Const Assertions in Literal Expressions in TypeScript
With TypeScript 3.4, const assertions were added to the language. A const assertion is a special kind of type assertion in which the const keyword is used instead of a type name.
Show full content

With TypeScript 3.4, const assertions were added to the language. A const assertion is a special kind of type assertion in which the const keyword is used instead of a type name. In this post, I'll explain how const assertions work and why we might want to use them.

Motivation for const Assertions

Let's say we've written the following fetchJSON function. It accepts a URL and an HTTP request method, uses the browser's Fetch API to make a GET or POST request to that URL, and deserializes the response as JSON:

function fetchJSON(url: string, method: "GET" | "POST") {
  return fetch(url, { method }).then(response => response.json());
}

We can call this function and pass an arbitrary URL to the url param and the string "GET" to the method param. Note that we're using two string literals here:

// OK, no type error
fetchJSON("https://example.com/", "GET").then(data => {
  // ...
});

To verify whether this function call is type-correct, TypeScript will check the types of all arguments of the function call against the parameter types defined in the function declaration. In this case, the types of both arguments are assignable to the parameter types, and therefore this function call type-checks correctly.

Let's now do a little bit of refactoring. The HTTP specification defines various additional request methods such as DELETE, HEAD, PUT, and others. We can define an HTTPRequestMethod enum-style mapping object and list the various request methods:

const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE",
};

Now we can replace the string literal "GET" in our fetchJSON function call by HTTPRequestMethod.GET:

fetchJSON("https://example.com/", HTTPRequestMethod.GET).then(data => {
  // ...
});

But now, TypeScript produces a type error! The type checker points out that the type of HTTPRequestMethod.GET is not assignable to the type of the method param:

// Error: Argument of type 'string' is not assignable
// to parameter of type '"GET" | "POST"'.

Why is that? HTTPRequestMethod.GET evaluates to the string "GET", the same value that we passed as an argument before. What's the difference between the types of the property HTTPRequestMethod.GET and the string literal "GET"? To answer that question, we have to understand how string literal types work and how TypeScript performs literal type widening.

String Literal Types

Let's look at the type of the value "GET" when we assign it to a variable declared using the const keyword:

// Type: "GET"
const httpRequestMethod = "GET";

TypeScript infers the type "GET" for our httpRequestMethod variable. "GET" is what's called a string literal type. Each literal type describes precisely one value, e.g. a specific string, number, boolean value, or enum member. In our case, we're dealing with the string value "GET", so our literal type is the string literal type "GET".

Notice that we've declared the httpRequestMethod variable using the const keyword. Therefore, we know that it's impossible to reassign the variable later; it'll always hold the value "GET". TypeScript understands that and automatically infers the string literal type "GET" to represent this piece of information in the type system.

Literal Type Widening

Let's now see what happens if we use the let keyword (instead of const) to declare the httpRequestMethod variable:

// Type: string
let httpRequestMethod = "GET";

TypeScript now performs what's known as literal type widening. The httpRequestMethod variable is inferred to have type string. We're initializing httpRequestMethod with the string "GET", but since the variable is declared using the let keyword, we can assign another value to it later:

// Type: string
let httpRequestMethod = "GET";

// OK, no type error
httpRequestMethod = "POST";

The later assignment of the value "POST" is type-correct since httpRequestMethod has type string. TypeScript inferred the type string because we most likely want to change the value of a variable declared using the let keyword later on. If we didn't want to reassign the variable, we should've used the const keyword instead.

Let's now look at our enum-style mapping object:

const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE",
};

What type does HTTPRequestMethod.GET have? Let's find out:

// Type: string
const httpRequestMethod = HTTPRequestMethod.GET;

TypeScript infers the type string for our httpRequestMethod variable. This is because we're initializing the variable with the value HTTPRequestMethod.GET (which has type string), so type string is inferred.

So why does HTTPRequestMethod.GET have type string and not type "GET"? We're initializing the GET property with the string literal "GET", and the HTTPRequestMethod object itself is defined using the const keyword. Shouldn't the resulting type be the string literal type "GET"?

The reason that TypeScript infers type string for HTTPRequestMethod.GET (and all the other properties) is that we could assign another value to any of the properties later on. To us, this object with its ALL_UPPERCASE property names looks like an enum which defines string constants that won't change over time. However, to TypeScript this is just a regular object with a few properties that happen to be initialized with string values.

The following example makes it a bit more obvious why TypeScript shouldn't infer a string literal type for object properties initialized with a string literal:

// Type: { name: string, jobTitle: string }
const person = {
  name: "Marius Schulz",
  jobTitle: "Software Engineer",
};

// OK, no type error
person.jobTitle = "Front End Engineer";

If the jobTitle property were inferred to have type "Software Engineer", it would be a type error if we tried to assign any string other than "Software Engineer" later on. Our assignment of "Front End Engineer" would not be type-correct. Object properties are mutable by default, so we wouldn't want TypeScript to infer a type which restricts us from performing perfectly valid mutations.

So how do we make the usage of our HTTPRequestMethod.GET property in the function call type-check correctly? We need to understand non-widening literal types first.

Non-Widening Literal Types

TypeScript has a special kind of literal type that's known as a non-widening literal type. As the name suggests, non-widening literal types will not be widened to a more generic type. For example, the non-widening string literal type "GET" will not be widened to string in cases where type widening would normally occur.

We can make the properties of our HTTPRequestMethod object receive a non-widening literal type by applying a type assertion of the corresponding string literal type to every property value:

const HTTPRequestMethod = {
  CONNECT: "CONNECT" as "CONNECT",
  DELETE: "DELETE" as "DELETE",
  GET: "GET" as "GET",
  HEAD: "HEAD" as "HEAD",
  OPTIONS: "OPTIONS" as "OPTIONS",
  PATCH: "PATCH" as "PATCH",
  POST: "POST" as "POST",
  PUT: "PUT" as "PUT",
  TRACE: "TRACE" as "TRACE",
};

Now, let's check the type of HTTPRequestMethod.GET again:

// Type: "GET"
const httpRequestMethod = HTTPRequestMethod.GET;

And indeed, now the httpRequestMethod variable has type "GET" rather than type string. The type of HTTPRequestMethod.GET (which is "GET") is assignable to the type of the method parameter (which is "GET" | "POST"), and therefore the fetchJSON function call will now type-check correctly:

// OK, no type error
fetchJSON("https://example.com/", HTTPRequestMethod.GET).then(data => {
  // ...
});

This is great news, but take a look at the number of type assertions we had to write to get to this point. That is a lot of noise! Every key/value pair now contains the name of the HTTP request method three times. Can we simplify this definition? Using TypeScript's const assertions feature, we most certainly can!

const Assertions for Literal Expressions

Our HTTPRequestMethod variable is initialized with a literal expression which is an object literal with several properties, all of which are initialized with string literals. As of TypeScript 3.4, we can apply a const assertion to a literal expression:

const HTTPRequestMethod = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE",
} as const;

A const assertion is a special type assertion that uses the const keyword instead of a specific type name. Using a const assertion on a literal expression has the following effects:

  1. No literal types in the literal expression will be widened.
  2. Object literals will get readonly properties.
  3. Array literals will become readonly tuples.

With the const assertion in place, the above definition of HTTPRequestMethod is equivalent to the following:

const HTTPRequestMethod: {
  readonly CONNECT: "CONNECT";
  readonly DELETE: "DELETE";
  readonly GET: "GET";
  readonly HEAD: "HEAD";
  readonly OPTIONS: "OPTIONS";
  readonly PATCH: "PATCH";
  readonly POST: "POST";
  readonly PUT: "PUT";
  readonly TRACE: "TRACE";
} = {
  CONNECT: "CONNECT",
  DELETE: "DELETE",
  GET: "GET",
  HEAD: "HEAD",
  OPTIONS: "OPTIONS",
  PATCH: "PATCH",
  POST: "POST",
  PUT: "PUT",
  TRACE: "TRACE",
};

We wouldn't want to have to write this definition by hand. It's verbose and contains a lot of repetition; notice that every HTTP request method is spelled out four times. The const assertion as const, on the other hand, is very succinct and the only bit of TypeScript-specific syntax in the entire example.

Also, observe that every property is now typed as readonly. If we try to assign a value to a read-only property, TypeScript will product a type error:

// Error: Cannot assign to 'GET'
// because it is a read-only property.
HTTPRequestMethod.GET = "...";

With the const assertion, we've given our HTTPRequestMethod object enum-like characteristics. But what about proper TypeScript enums?

Using TypeScript Enums

Another possible solution would've been to use a TypeScript enum instead of a plain object literal. We could've defined HTTPRequestMethod using the enum keyword like this:

enum HTTPRequestMethod {
  CONNECT = "CONNECT",
  DELETE = "DELETE",
  GET = "GET",
  HEAD = "HEAD",
  OPTIONS = "OPTIONS",
  PATCH = "PATCH",
  POST = "POST",
  PUT = "PUT",
  TRACE = "TRACE",
}

TypeScript enums are meant to describe named constants, which is why their members are always read-only. Members of a string enum have a string literal type:

// Type: "GET"
const httpRequestMethod = HTTPRequestMethod.GET;

This means our function call will type-check when we pass HTTPRequestMethod.GET as an argument for the method parameter:

// OK, no type error
fetchJSON("https://example.com/", HTTPRequestMethod.GET).then(data => {
  // ...
});

However, some developers don't like to use TypeScript enums in their code because the enum syntax is not valid JavaScript on its own. The TypeScript compiler will emit the following JavaScript code for our HTTPRequestMethod enum defined above:

var HTTPRequestMethod;
(function (HTTPRequestMethod) {
  HTTPRequestMethod["CONNECT"] = "CONNECT";
  HTTPRequestMethod["DELETE"] = "DELETE";
  HTTPRequestMethod["GET"] = "GET";
  HTTPRequestMethod["HEAD"] = "HEAD";
  HTTPRequestMethod["OPTIONS"] = "OPTIONS";
  HTTPRequestMethod["PATCH"] = "PATCH";
  HTTPRequestMethod["POST"] = "POST";
  HTTPRequestMethod["PUT"] = "PUT";
  HTTPRequestMethod["TRACE"] = "TRACE";
})(HTTPRequestMethod || (HTTPRequestMethod = {}));

It's entirely up to you to decide whether you want to use plain object literals or proper TypeScript enums. If you want to stay as close to JavaScript as possible and only use TypeScript for type annotations, you can stick with plain object literals and const assertions. If you don't mind using non-standard syntax for defining enums and you like the brevity, TypeScript enums could be a good choice.

const Assertions for Other Types

You can apply a const assertion to …

  • string literals,
  • numeric literals,
  • boolean literals,
  • array literals, and
  • object literals.

For example, you could define an ORIGIN variable describing the origin in 2-dimensional space like this:

const ORIGIN = {
  x: 0,
  y: 0,
} as const;

This is equivalent to (and much more succinct than) the following declaration:

const ORIGIN: {
  readonly x: 0;
  readonly y: 0;
} = {
  x: 0,
  y: 0,
};

Alternatively, you could've modeled the representation of a point as a tuple of the X and Y coordinates:

// Type: readonly [0, 0]
const ORIGIN = [0, 0] as const;

Because of the const assertion, ORIGIN is typed as readonly [0, 0]. Without the assertion, ORIGIN would've been inferred to have type number[] instead:

// Type: number[]
const ORIGIN = [0, 0];
urn:uuid:30b233ee-3837-4073-acf1-868bfe8796ed
Extensions