Learn/Recipe/Narrow nodes in TypeScript

How to narrow Nodes in TypeScript

To work with a specific node type or a set of node types we need to narrow their type. For example, we can take a Node and do a type safe check to get a more specific type like a Link. unified provides a utility to help with this. TypeScript also provides some language features that can help. Let’s first take a look at unist-util-is.

unist-util-is takes a Node and a Test and returns whether the test passes. It can be used as a TypeScript type predicate which when used as a condition (such as in an if-statement) tells TypeScript to narrow a node.

Here are some ways to narrow nodes:

import type {Node, Literal} from 'unist'
import type {List, Blockquote, Strong, Emphasis, Heading} from 'mdast'
import {is, convert} from 'unist-util-is'

// `Node` could come from a plugin, a utility, or be passed into a function
// here we hard code a Node for testing purposes
const node: Node = {type: 'example'}

if (is<List>(node, 'list')) {
  // If we’re here, node is List.
  // `'list'` is compared to `node.type` to make sure they match.
  // `true` means a match, `false` means no match.
  // `<List>` tells TypeScript to ensure `'list'` matches `List.type` and that
  // if `'list'` matches both `node.type` and `List.type`, we know that node is
  // `List` within this if condition.

if (is<Strong | Emphasis>(node, ['strong', 'emphasis'])) {
  // If we get here, node is `Strong` or `Emphasis`.

  // If we want even more specific type, we can use a discriminated union
  // <https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions>
  if (node.type === 'emphasis') {
    // If we get here, node is `Emphasis`.

if (is<Heading>(node, {type: 'heading', depth: 1})) {
  // If we get here, node is `Heading`.
  // TypeScript checks that the properties used in the `Test` are valid
  // attributes of `<Heading>`.
  // It does not narrow `node.depth` only be 1, which can be done with
  // `<Heading & {depth: 1}>`.

// For advanced use cases, another predicate can be passed to `is`
if (is<Literal>(node, (node: Node): node is Literal => 'value' in node)) {
  // If we get here, node is one of the `Literal` types.
  // Here any comparison function can be used, as long as it is a predicate
  // <https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates>
  // and as long as the predicate and generic match.
  // For example here, `<Literal>` and `is Literal` match.

// Reusable predicates can also be created using any `Test`
const isBlockquote = convert<Blockquote>('blockquote')
if (isBlockquote(node)) {
  // If we get here, node is `Blockquote`.