Repository: eemeli/yaml
Pull request #240 (2021-03-12 13:38:52)

Drop `type` property from all but Scalar nodes
Originally, `type` was a thing that was carried into the AST nodes from the previous CST parser. Now, it's a clumsy & sometimes misleading API. So let's drop it elsewhere, but leave it for scalars where it still makes good sense. This is yet another **BREAKING CHANGE**.

For scalars, let's move the constants from e.g. `Type.BLOCK_LITERAL` to `Scalar.BLOCK_LITERAL`. Otherwise, everything stays pretty much the same.

For collections, we need to add a new `flow: boolean` property that can now be more easily set or toggled to control whether to use block or flow representation. As before with `type: 'FLOW_MAP' | 'FLOW_SEQ'`, setting `flow: true` will force that choice for all of its child nodes as well.

Dropping the `type` values also made it clear that Merge is no longer a good idea as a separate class; dropping it allows for merge nodes to be duck-typed when `merge: true` or `version: '1.1'` is set.

```diff
-import { Scalar, Type, YAMLMap, YAMLSeq } from 'yaml'
+import { isMap, isSeq, Scalar, YAMLMap, YAMLSeq } from 'yaml'

let scalar: Scalar
-scalar.type = Type.BLOCK_LITERAL
+scalar.type = Scalar.BLOCK_LITERAL

let map: YAMLMap
-map.type = Type.FLOW_MAP
+map.flow = true
-const mapIsMap = map.type === Type.FLOW_MAP || map.type === Type.MAP // only if parsed
+const mapIsMap = isMap(map)

let seq: YAMLSeq
-seq.type = Type.FLOW_SEQ
+seq.flow = true
-const seqIsSeq = seq.type === Type.FLOW_SEQ || seq.type === Type.SEQ // only if parsed
+const seqIsSeq = isSeq(seq)
```


Patch:
diff --git a/docs/05_content_nodes.md b/docs/05_content_nodes.md
index 636ad67..c7dcde6 100644
--- a/docs/05_content_nodes.md
+++ b/docs/05_content_nodes.md
@@ -5,62 +5,84 @@ After parsing, the `contents` value of each `YAML.Document` is the root of an [A
 ## Scalar Values
 
 ```js
-class Node {
-  comment: ?string,   // a comment on or immediately after this
-  commentBefore: ?string, // a comment before this
-  range: ?[number, number],
+class NodeBase {
+  comment?: string,   // a comment on or immediately after this
+  commentBefore?: string, // a comment before this
+  range?: [number, number],
       // the [start, end] range of characters of the source parsed
       // into this node (undefined for pairs or if not parsed)
-  spaceBefore: ?boolean,
+  spaceBefore?: boolean,
       // a blank line before this node and its commentBefore
-  tag: ?string,       // a fully qualified tag, if required
+  tag?: string,       // a fully qualified tag, if required
   toJSON(): any       // a plain JS or JSON representation of this node
 }
 ```
 
-For scalar values, the `tag` will not be set unless it was explicitly defined in the source document; this also applies for unsupported tags that have been resolved using a fallback tag (string, `Map`, or `Seq`).
+For scalar values, the `tag` will not be set unless it was explicitly defined in the source document; this also applies for unsupported tags that have been resolved using a fallback tag (string, `YAMLMap`, or `YAMLSeq`).
 
 ```js
-class Scalar extends Node {
-  format: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined,
+class Scalar<T = unknown> extends NodeBase {
+  format?: 'BIN' | 'HEX' | 'OCT' | 'TIME' | undefined,
       // By default (undefined), numbers use decimal notation.
       // The YAML 1.2 core schema only supports 'HEX' and 'OCT'.
-  type:
+  type?:
     'BLOCK_FOLDED' | 'BLOCK_LITERAL' | 'PLAIN' |
     'QUOTE_DOUBLE' | 'QUOTE_SINGLE' | undefined,
-  value: any
+  value: T
 }
 ```
 
-A parsed document's contents will have all of its non-object values wrapped in `Scalar` objects, which themselves may be in some hierarchy of `Map` and `Seq` collections. However, this is not a requirement for the document's stringification, which is rather tolerant regarding its input values, and will use [`doc.createNode()`](#creating-nodes) when encountering an unwrapped value.
+A parsed document's contents will have all of its non-object values wrapped in `Scalar` objects, which themselves may be in some hierarchy of `YAMLMap` and `YAMLSeq` collections.
+However, this is not a requirement for the document's stringification, which is rather tolerant regarding its input values, and will use [`doc.createNode()`](#creating-nodes) when encountering an unwrapped value.
 
-When stringifying, the node `type` will be taken into account by `!!str` and `!!binary` values, and ignored by other scalars. On the other hand, `!!int` and `!!float` stringifiers will take `format` into account.
+When stringifying, the node `type` will be taken into account by `!!str` and `!!binary` values, and ignored by other scalars.
+On the other hand, `!!int` and `!!float` stringifiers will take `format` into account.
 
 ## Collections
 
 ```js
-class Pair extends Node {
-  key: Node | any,    // key and value are always Node or null
-  value: Node | any,  // when parsed, but can be set to anything
-  type: 'PAIR'
+class Pair<K = unknown, V = unknown> extends NodeBase {
+  key: K,   // When parsed, key and value are always
+  value: V  // Node or null, but can be set to anything
 }
 
-class Map extends Node {
-  items: Array<Pair>,
-  type: 'FLOW_MAP' | 'MAP' | undefined
+class Collection extends NodeBase {
+  flow?: boolean // use flow style when stringifying this
+  schema?: Schema
+  addIn(path: Iterable<unknown>, value: unknown): void
+  deleteIn(path: Iterable<unknown>): boolean
+  getIn(path: Iterable<unknown>, keepScalar?: boolean): unknown
+  hasIn(path: Iterable<unknown>): boolean
+  setIn(path: Iterable<unknown>, value: unknown): void
 }
 
-class Seq extends Node {
-  items: Array<Node | any>,
-  type: 'FLOW_SEQ' | 'SEQ' | undefined
+class YAMLMap<K = unknown, V = unknown> extends Collection {
+  items: Pair<K, V>[]
+  add(pair: Pair<K, V> | { key: K; value: V }, overwrite?: boolean): void
+  delete(key: K): boolean
+  get(key: K, keepScalar?: boolean): unknown
+  has(key: K): boolean
+  set(key: K, value: V): void
+}
+
+class YAMLSeq<T = unknown> extends Collection {
+  items: T[]
+  add(value: T): void
+  delete(key: number | Scalar<number>): boolean
+  get(key: number | Scalar<number>, keepScalar?: boolean): unknown
+  has(key: number | Scalar<number>): boolean
+  set(key: number | Scalar<number>, value: T): void
 }
 ```
 
-Within all YAML documents, two forms of collections are supported: sequential `Seq` collections and key-value `Map` collections. The JavaScript representations of these collections both have an `items` array, which may (`Seq`) or must (`Map`) consist of `Pair` objects that contain a `key` and a `value` of any type, including `null`. The `items` array of a `Seq` object may contain values of any type.
+Within all YAML documents, two forms of collections are supported: sequential `YAMLSeq` collections and key-value `YAMLMap` collections.
+The JavaScript representations of these collections both have an `items` array, which may (`YAMLSeq`) or must (`YAMLMap`) consist of `Pair` objects that contain a `key` and a `value` of any type, including `null`.
+The `items` array of a `YAMLSeq` object may contain values of any type.
 
-When stringifying collections, by default block notation will be used. Flow notation will be selected if `type` is `FLOW_MAP` or `FLOW_SEQ`, the collection is within a surrounding flow collection, or if the collection is in an implicit key.
+When stringifying collections, by default block notation will be used.
+Flow notation will be selected if `flow` is `true`, the collection is within a surrounding flow collection, or if the collection is in an implicit key.
 
-The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/index.html) that are based on `Map` and `Seq`: `OMap` and `Pairs` are sequences of `Pair` objects (`OMap` requires unique keys & corresponds to the JS Map object), and `Set` is a map of keys with null values that corresponds to the JS Set object.
+The `yaml-1.1` schema includes [additional collections](https://yaml.org/type/index.html) that are based on `YAMLMap` and `YAMLSeq`: `OMap` and `Pairs` are sequences of `Pair` objects (`OMap` requires unique keys & corresponds to the JS Map object), and `Set` is a map of keys with null values that corresponds to the JS Set object.
 
 All of the collections provide the following accessor methods:
 
@@ -90,9 +112,13 @@ doc.has('c') // false
 doc.hasIn(['b', '0']) // true
 ```
 
-For all of these methods, the keys may be nodes or their wrapped scalar values (i.e. `42` will match `Scalar { value: 42 }`) . Keys for `!!seq` should be positive integers, or their string representations. `add()` and `set()` do not automatically call `doc.createNode()` to wrap the value.
+For all of these methods, the keys may be nodes or their wrapped scalar values (i.e. `42` will match `Scalar { value: 42 }`).
+Keys for `!!seq` should be positive integers, or their string representations.
+`add()` and `set()` do not automatically call `doc.createNode()` to wrap the value.
 
-Each of the methods also has a variant that requires an iterable as the first parameter, and allows fetching or modifying deeper collections. If any intermediate node in `path` is a scalar rather than a collection, an error will be thrown. If any of the intermediate collections is not found:
+Each of the methods also has a variant that requires an iterable as the first parameter, and allows fetching or modifying deeper collections.
+If any intermediate node in `path` is a scalar rather than a collection, an error will be thrown.
+If any of the intermediate collections is not found:
 
 - `getIn` and `hasIn` will return `undefined` or `false` (respectively)
 - `addIn` and `setIn` will create missing collections; non-negative integer keys will create sequences, all other keys create maps
@@ -103,9 +129,8 @@ Note that for `addIn` the path argument points to the collection rather than the
 ## Alias Nodes
 
 ```js
-class Alias extends Node {
-  source: Scalar | Map | Seq,
-  type: 'ALIAS'
+class Alias extends NodeBase {
+  source: Scalar | YAMLMap | YAMLSeq
 }
 
 const obj = YAML.parse('[ &x { X: 42 }, Y, *x ]')
@@ -127,8 +152,7 @@ When nodes are constructed from JS structures (e.g. during `YAML.stringify()`),
 ```js
 class Merge extends Pair {
   key: Scalar('<<'),      // defined by the type specification
-  value: Seq<Alias(Map)>, // stringified as *A if length = 1
-  type: 'MERGE_PAIR'
+  value: Seq<Alias(Map)>  // stringified as *A if length = 1
 }
 ```
 
@@ -205,7 +229,9 @@ doc.toString()
 
 To construct a `YAMLSeq` or `YAMLMap`, use `new Document()` or `doc.createNode()` with array, object or iterable input, or create the collections directly by importing the classes from `yaml`.
 
-Once created, normal array operations may be used to modify the `items` array. New `Pair` objects may created either by importing the class from `yaml` and using its `new Pair(key, value)` constructor, or by using the `doc.createPair(key, value, options?)` method. The latter will recursively wrap the `key` and `value` as nodes, and accepts the same options as `doc.createNode()`
+Once created, normal array operations may be used to modify the `items` array.
+New `Pair` objects may created either by importing the class from `yaml` and using its `new Pair(key, value)` constructor, or by using the `doc.createPair(key, value, options?)` method.
+The latter will recursively wrap the `key` and `value` as nodes, and accepts the same options as `doc.createNode()`
 
 ## Modifying Nodes
 
@@ -243,7 +269,7 @@ String(doc)
 // - 1: 'a number'
 ```
 
-In general, it's safe to modify nodes manually, e.g. splicing the `items` array of a `YAMLMap` or changing its `type` from `'MAP'` to `'FLOW_MAP'`.
+In general, it's safe to modify nodes manually, e.g. splicing the `items` array of a `YAMLMap` or setting its `flow` value to `true`.
 For operations on nodes at a known location in the tree, it's probably easiest to use `doc.getIn(path, true)` to access them.
 For more complex or general operations, a visitor API is provided:
 
diff --git a/src/compose/compose-collection.ts b/src/compose/compose-collection.ts
index 8e12230..98adc88 100644
--- a/src/compose/compose-collection.ts
+++ b/src/compose/compose-collection.ts
@@ -1,6 +1,5 @@
-import { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
-import { isNode, ParsedNode } from '../nodes/Node.js'
+import { isMap, isNode, ParsedNode } from '../nodes/Node.js'
 import { Scalar } from '../nodes/Scalar.js'
 import type { YAMLMap } from '../nodes/YAMLMap.js'
 import type { YAMLSeq } from '../nodes/YAMLSeq.js'
@@ -48,22 +47,7 @@ export function composeCollection(
     return coll
   }
 
-  let expType: 'map' | 'seq' // | null = null
-  switch (coll.type) {
-    case Type.FLOW_MAP:
-    case Type.MAP:
-      expType = 'map'
-      break
-    case Type.FLOW_SEQ:
-    case Type.SEQ:
-      expType = 'seq'
-      break
-    default:
-      onError(coll.range[0], `Unexpected collection type: ${coll.type}`)
-      coll.tag = tagName
-      return coll
-  }
-
+  const expType = isMap(coll) ? 'map' : 'seq'
   let tag = doc.schema.tags.find(
     t => t.collection === expType && t.tag === tagName
   ) as CollectionTag | undefined
diff --git a/src/compose/composer.ts b/src/compose/composer.ts
index ab5b53a..1ff0754 100644
--- a/src/compose/composer.ts
+++ b/src/compose/composer.ts
@@ -78,11 +78,7 @@ export class Composer {
         doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment
       } else if (afterEmptyLine || doc.directives.marker || !dc) {
         doc.commentBefore = comment
-      } else if (
-        isCollection(dc) &&
-        (dc.type === 'MAP' || dc.type === 'SEQ') &&
-        dc.items.length > 0
-      ) {
+      } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) {
         const it = dc.items[0]
         const cb = it.commentBefore
         it.commentBefore = cb ? `${comment}\n${cb}` : comment
diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts
index e3d27d8..8be9bc1 100644
--- a/src/compose/resolve-block-map.ts
+++ b/src/compose/resolve-block-map.ts
@@ -1,10 +1,8 @@
-import { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
 import { Pair } from '../nodes/Pair.js'
 import { YAMLMap } from '../nodes/YAMLMap.js'
 import type { BlockMap } from '../parse/tokens.js'
 import type { ComposeNode } from './compose-node.js'
-import { resolveMergePair } from './resolve-merge-pair.js'
 import { resolveProps } from './resolve-props.js'
 import { containsNewline } from './util-contains-newline.js'
 
@@ -19,7 +17,6 @@ export function resolveBlockMap(
 ) {
   const start = offset
   const map = new YAMLMap(doc.schema)
-  map.type = Type.MAP
   if (anchor) doc.anchors.setAnchor(map, anchor)
 
   for (const { start, key, sep, value } of items) {
@@ -92,8 +89,7 @@ export function resolveBlockMap(
         ? composeNode(doc, value, valueProps, onError)
         : composeEmptyNode(doc, offset, sep, null, valueProps, onError)
       offset = valueNode.range[1]
-      const pair = new Pair(keyNode, valueNode)
-      map.items.push(doc.schema.merge ? resolveMergePair(pair, onError) : pair)
+      map.items.push(new Pair(keyNode, valueNode))
     } else {
       // key with no value
       if (implicitKey)
diff --git a/src/compose/resolve-block-scalar.ts b/src/compose/resolve-block-scalar.ts
index 47234a5..8ccff53 100644
--- a/src/compose/resolve-block-scalar.ts
+++ b/src/compose/resolve-block-scalar.ts
@@ -1,4 +1,4 @@
-import { Type } from '../constants.js'
+import { Scalar } from '../nodes/Scalar.js'
 import type { BlockScalar } from '../parse/tokens.js'
 
 export function resolveBlockScalar(
@@ -7,13 +7,13 @@ export function resolveBlockScalar(
   onError: (offset: number, message: string) => void
 ): {
   value: string
-  type: Type.BLOCK_FOLDED | Type.BLOCK_LITERAL | null
+  type: Scalar.BLOCK_FOLDED | Scalar.BLOCK_LITERAL | null
   comment: string
   length: number
 } {
   const header = parseBlockScalarHeader(scalar, strict, onError)
   if (!header) return { value: '', type: null, comment: '', length: 0 }
-  const type = header.mode === '>' ? Type.BLOCK_FOLDED : Type.BLOCK_LITERAL
+  const type = header.mode === '>' ? Scalar.BLOCK_FOLDED : Scalar.BLOCK_LITERAL
   const lines = scalar.source ? splitLines(scalar.source) : []
 
   // determine the end of content & start of chomping
@@ -79,7 +79,7 @@ export function resolveBlockScalar(
       indent = ''
     }
 
-    if (type === Type.BLOCK_LITERAL) {
+    if (type === Scalar.BLOCK_LITERAL) {
       value += sep + indent.slice(trimIndent) + content
       sep = '\n'
     } else if (indent.length > trimIndent || content[0] === '\t') {
diff --git a/src/compose/resolve-block-seq.ts b/src/compose/resolve-block-seq.ts
index 5b7c692..8832e2f 100644
--- a/src/compose/resolve-block-seq.ts
+++ b/src/compose/resolve-block-seq.ts
@@ -1,4 +1,3 @@
-import { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
 import { YAMLSeq } from '../nodes/YAMLSeq.js'
 import type { BlockSequence } from '../parse/tokens.js'
@@ -14,7 +13,6 @@ export function resolveBlockSeq(
 ) {
   const start = offset
   const seq = new YAMLSeq(doc.schema)
-  seq.type = Type.SEQ
   if (anchor) doc.anchors.setAnchor(seq, anchor)
   for (const { start, value } of items) {
     const props = resolveProps(
diff --git a/src/compose/resolve-flow-collection.ts b/src/compose/resolve-flow-collection.ts
index 193c903..f683ce2 100644
--- a/src/compose/resolve-flow-collection.ts
+++ b/src/compose/resolve-flow-collection.ts
@@ -1,4 +1,3 @@
-import { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
 import { isNode, isPair, ParsedNode } from '../nodes/Node.js'
 import { Pair } from '../nodes/Pair.js'
@@ -7,7 +6,6 @@ import { YAMLSeq } from '../nodes/YAMLSeq.js'
 import type { FlowCollection, SourceToken, Token } from '../parse/tokens.js'
 import type { ComposeNode } from './compose-node.js'
 import { resolveEnd } from './resolve-end.js'
-import { resolveMergePair } from './resolve-merge-pair.js'
 import { containsNewline } from './util-contains-newline.js'
 
 export function resolveFlowCollection(
@@ -19,7 +17,7 @@ export function resolveFlowCollection(
 ) {
   const isMap = fc.start.source === '{'
   const coll = isMap ? new YAMLMap(doc.schema) : new YAMLSeq(doc.schema)
-  coll.type = isMap ? Type.FLOW_MAP : Type.FLOW_SEQ
+  coll.flow = true
   if (_anchor) doc.anchors.setAnchor(coll, _anchor)
 
   let key: ParsedNode | null = null
@@ -60,13 +58,12 @@ export function resolveFlowCollection(
       value = composeEmptyNode(doc, offset, fc.items, pos, getProps(), onError)
     }
     if (isMap || atExplicitKey) {
-      const pair = key ? new Pair(key, value) : new Pair(value)
-      coll.items.push(doc.schema.merge ? resolveMergePair(pair, onError) : pair)
+      coll.items.push(key ? new Pair(key, value) : new Pair(value))
     } else {
       const seq = coll as YAMLSeq
       if (key) {
         const map = new YAMLMap(doc.schema)
-        map.type = Type.FLOW_MAP
+        map.flow = true
         map.items.push(new Pair(key, value))
         seq.items.push(map)
       } else seq.items.push(value)
@@ -143,7 +140,7 @@ export function resolveFlowCollection(
           if (value) {
             onError(offset, 'Missing {} around pair used as mapping key')
             const map = new YAMLMap(doc.schema)
-            map.type = Type.FLOW_MAP
+            map.flow = true
             map.items.push(new Pair(key, value))
             map.range = [key.range[0], value.range[1]]
             key = map as YAMLMap.Parsed
diff --git a/src/compose/resolve-flow-scalar.ts b/src/compose/resolve-flow-scalar.ts
index cea539c..651a29b 100644
--- a/src/compose/resolve-flow-scalar.ts
+++ b/src/compose/resolve-flow-scalar.ts
@@ -1,4 +1,4 @@
-import { Type } from '../constants.js'
+import { Scalar } from '../nodes/Scalar.js'
 import type { FlowScalar } from '../parse/tokens.js'
 import { resolveEnd } from './resolve-end.js'
 
@@ -8,26 +8,26 @@ export function resolveFlowScalar(
   onError: (offset: number, message: string) => void
 ): {
   value: string
-  type: Type.PLAIN | Type.QUOTE_DOUBLE | Type.QUOTE_SINGLE | null
+  type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE | null
   comment: string
   length: number
 } {
-  let _type: Type.PLAIN | Type.QUOTE_DOUBLE | Type.QUOTE_SINGLE
+  let _type: Scalar.PLAIN | Scalar.QUOTE_DOUBLE | Scalar.QUOTE_SINGLE
   let value: string
   const _onError = (rel: number, msg: string) => onError(offset + rel, msg)
   switch (type) {
     case 'scalar':
-      _type = Type.PLAIN
+      _type = Scalar.PLAIN
       value = plainValue(source, _onError)
       break
 
     case 'single-quoted-scalar':
-      _type = Type.QUOTE_SINGLE
+      _type = Scalar.QUOTE_SINGLE
       value = singleQuotedValue(source, _onError)
       break
 
     case 'double-quoted-scalar':
-      _type = Type.QUOTE_DOUBLE
+      _type = Scalar.QUOTE_DOUBLE
       value = doubleQuotedValue(source, _onError)
       break
 
diff --git a/src/compose/resolve-merge-pair.ts b/src/compose/resolve-merge-pair.ts
deleted file mode 100644
index 6651595..0000000
--- a/src/compose/resolve-merge-pair.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Alias } from '../nodes/Alias.js'
-import { Merge } from '../nodes/Merge.js'
-import { isAlias, isMap, isScalar, ParsedNode } from '../nodes/Node.js'
-import type { Pair } from '../nodes/Pair.js'
-import type { Scalar } from '../nodes/Scalar.js'
-
-export function resolveMergePair(
-  pair: Pair,
-  onError: (offset: number, message: string) => void
-) {
-  if (!isScalar(pair.key) || pair.key.value !== Merge.KEY) return pair
-
-  const merge = new Merge(pair as Pair<Scalar, Alias>)
-  for (const node of merge.value.items as Alias.Parsed[]) {
-    if (isAlias(node)) {
-      if (isMap(node.source)) {
-        // ok
-      } else {
-        onError(node.range[0], 'Merge nodes aliases can only point to maps')
-      }
-    } else {
-      onError(
-        (node as ParsedNode).range[0],
-        'Merge nodes can only have Alias nodes as values'
-      )
-    }
-  }
-  return merge
-}
diff --git a/src/constants.ts b/src/constants.ts
deleted file mode 100644
index bfe885e..0000000
--- a/src/constants.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-export type LogLevelId = 'silent' | 'error' | 'warn' | 'debug'
-
-export const Char = {
-  ANCHOR: '&',
-  COMMENT: '#',
-  TAG: '!',
-  DIRECTIVES_END: '-',
-  DOCUMENT_END: '.'
-}
-
-export const LogLevel = Object.assign<
-  LogLevelId[],
-  { SILENT: 0; ERROR: 1; WARN: 2; DEBUG: 3 }
->(['silent', 'error', 'warn', 'debug'], {
-  SILENT: 0,
-  ERROR: 1,
-  WARN: 2,
-  DEBUG: 3
-})
-
-export enum Type {
-  ALIAS = 'ALIAS',
-  BLANK_LINE = 'BLANK_LINE',
-  BLOCK_FOLDED = 'BLOCK_FOLDED',
-  BLOCK_LITERAL = 'BLOCK_LITERAL',
-  COMMENT = 'COMMENT',
-  DIRECTIVE = 'DIRECTIVE',
-  DOCUMENT = 'DOCUMENT',
-  FLOW_MAP = 'FLOW_MAP',
-  FLOW_SEQ = 'FLOW_SEQ',
-  MAP = 'MAP',
-  MAP_KEY = 'MAP_KEY',
-  MAP_VALUE = 'MAP_VALUE',
-  PLAIN = 'PLAIN',
-  QUOTE_DOUBLE = 'QUOTE_DOUBLE',
-  QUOTE_SINGLE = 'QUOTE_SINGLE',
-  SEQ = 'SEQ',
-  SEQ_ITEM = 'SEQ_ITEM'
-}
-
-export const defaultTagPrefix = 'tag:yaml.org,2002:'
-export const defaultTags = {
-  MAP: 'tag:yaml.org,2002:map',
-  SEQ: 'tag:yaml.org,2002:seq',
-  STR: 'tag:yaml.org,2002:str'
-}
diff --git a/src/doc/Anchors.ts b/src/doc/Anchors.ts
index 0b030cc..660a570 100644
--- a/src/doc/Anchors.ts
+++ b/src/doc/Anchors.ts
@@ -1,6 +1,8 @@
 import { Alias } from '../nodes/Alias.js'
-import { Merge } from '../nodes/Merge.js'
 import { isAlias, isCollection, isMap, isScalar, Node } from '../nodes/Node.js'
+import { Pair } from '../nodes/Pair.js'
+import { Scalar } from '../nodes/Scalar.js'
+import { YAMLSeq } from '../nodes/YAMLSeq.js'
 
 export class Anchors {
   map: Record<string, Node> = Object.create(null)
@@ -20,12 +22,12 @@ export class Anchors {
   }
 
   /**
-   * Create a new `Merge` node with the given source nodes.
+   * Create a new merge `Pair` with the given source nodes.
    * Non-`Alias` sources will be automatically wrapped.
    */
   createMergePair(...sources: Node[]) {
-    const merge = new Merge()
-    merge.value.items = sources.map(s => {
+    const key = new Scalar(Pair.MERGE_KEY)
+    const items = sources.map(s => {
       if (isAlias(s)) {
         if (isMap(s.source)) return s
       } else if (isMap(s)) {
@@ -33,7 +35,10 @@ export class Anchors {
       }
       throw new Error('Merge sources must be Map nodes or their Aliases')
     })
-    return merge
+    if (items.length === 1) return new Pair(key, items[0])
+    const seq = new YAMLSeq<Alias>()
+    seq.items = items
+    return new Pair(key, seq)
   }
 
   /** The anchor name associated with `node`, if set. */
diff --git a/src/doc/Document.ts b/src/doc/Document.ts
index 2f127d9..4661a98 100644
--- a/src/doc/Document.ts
+++ b/src/doc/Document.ts
@@ -1,14 +1,11 @@
-import { Type } from '../constants.js'
 import type { YAMLError, YAMLWarning } from '../errors.js'
 import { Alias } from '../nodes/Alias.js'
 import { collectionFromPath, isEmptyPath } from '../nodes/Collection.js'
 import {
   DOC,
   isCollection,
-  isMap,
   isNode,
   isScalar,
-  isSeq,
   Node,
   NODE_TYPE,
   ParsedNode
@@ -94,8 +91,6 @@ export class Document<T = unknown> {
    */
   tagPrefixes: Document.TagPrefix[] = []
 
-  type: Type.DOCUMENT = Type.DOCUMENT
-
   /** Warnings encountered during parsing. */
   warnings: YAMLWarning[] = []
 
@@ -190,10 +185,7 @@ export class Document<T = unknown> {
         this.anchors.map[name] = alias.source
       }
     }
-    if (flow) {
-      if (isMap(node)) node.type = Type.FLOW_MAP
-      else if (isSeq(node)) node.type = Type.FLOW_SEQ
-    }
+    if (flow && isCollection(node)) node.flow = true
 
     return node
   }
diff --git a/src/doc/createNode.ts b/src/doc/createNode.ts
index d36e976..3d632cd 100644
--- a/src/doc/createNode.ts
+++ b/src/doc/createNode.ts
@@ -1,4 +1,3 @@
-import { defaultTagPrefix } from '../constants.js'
 import type { Alias } from '../nodes/Alias.js'
 import { isNode, isPair, Node } from '../nodes/Node.js'
 import { Scalar } from '../nodes/Scalar.js'
@@ -7,6 +6,8 @@ import type { TagObj } from '../tags/types.js'
 import type { Replacer } from './Document.js'
 import type { Schema } from './Schema.js'
 
+const defaultTagPrefix = 'tag:yaml.org,2002:'
+
 export interface CreateNodeAliasRef {
   node: Node | undefined
   value: unknown
diff --git a/src/index.ts b/src/index.ts
index 5195dc0..aafb304 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,4 @@
 export { Composer } from './compose/composer.js'
-export { Type } from './constants.js'
 
 export { Document } from './doc/Document.js'
 export { Schema } from './doc/Schema.js'
@@ -7,7 +6,6 @@ export { Schema } from './doc/Schema.js'
 export { YAMLError, YAMLParseError, YAMLWarning } from './errors.js'
 
 export { Alias } from './nodes/Alias.js'
-export { Merge } from './nodes/Merge.js'
 export {
   isAlias,
   isCollection,
@@ -20,7 +18,7 @@ export {
   Node,
   ParsedNode
 } from './nodes/Node.js'
-export { Pair, PairType } from './nodes/Pair.js'
+export { Pair } from './nodes/Pair.js'
 export { Scalar } from './nodes/Scalar.js'
 export { YAMLMap } from './nodes/YAMLMap.js'
 export { YAMLSeq } from './nodes/YAMLSeq.js'
diff --git a/src/log.ts b/src/log.ts
index b9df8a9..86fbabb 100644
--- a/src/log.ts
+++ b/src/log.ts
@@ -1,11 +1,11 @@
-import { LogLevel, LogLevelId } from './constants.js'
+export type LogLevelId = 'silent' | 'error' | 'warn' | 'debug'
 
 export function debug(logLevel: LogLevelId, ...messages: any[]) {
-  if (LogLevel.indexOf(logLevel) >= LogLevel.DEBUG) console.log(...messages)
+  if (logLevel === 'debug') console.log(...messages)
 }
 
 export function warn(logLevel: LogLevelId, warning: string | Error) {
-  if (LogLevel.indexOf(logLevel) >= LogLevel.WARN) {
+  if (logLevel === 'debug' || logLevel === 'warn') {
     if (typeof process !== 'undefined' && process.emitWarning)
       process.emitWarning(warning)
     else console.warn(warning)
diff --git a/src/nodes/Alias.ts b/src/nodes/Alias.ts
index 975eaca..130b9ba 100644
--- a/src/nodes/Alias.ts
+++ b/src/nodes/Alias.ts
@@ -1,4 +1,3 @@
-import { Type } from '../constants.js'
 import { StringifyContext } from '../stringify/stringify.js'
 import { ALIAS, isAlias, isCollection, isPair, Node, NodeBase } from './Node.js'
 import { toJS, ToJSContext } from './toJS.js'
@@ -11,7 +10,6 @@ export declare namespace Alias {
 
 export class Alias<T extends Node = Node> extends NodeBase {
   source: T
-  type: Type.ALIAS = Type.ALIAS
 
   constructor(source: T) {
     super(ALIAS)
diff --git a/src/nodes/Collection.ts b/src/nodes/Collection.ts
index b91e1ad..d034003 100644
--- a/src/nodes/Collection.ts
+++ b/src/nodes/Collection.ts
@@ -1,9 +1,15 @@
-import { Type } from '../constants.js'
 import { createNode } from '../doc/createNode.js'
 import type { Schema } from '../doc/Schema.js'
 import { addComment } from '../stringify/addComment.js'
 import { stringify, StringifyContext } from '../stringify/stringify.js'
-import { isCollection, isNode, isPair, isScalar, NodeBase, NODE_TYPE } from './Node.js'
+import {
+  isCollection,
+  isNode,
+  isPair,
+  isScalar,
+  NodeBase,
+  NODE_TYPE
+} from './Node.js'
 import type { Pair } from './Pair.js'
 
 export function collectionFromPath(
@@ -64,12 +70,11 @@ export abstract class Collection extends NodeBase {
 
   declare items: unknown[]
 
-  declare type?:
-    | Type.MAP
-    | Type.FLOW_MAP
-    | Type.SEQ
-    | Type.FLOW_SEQ
-    | Type.DOCUMENT
+  /**
+   * If true, stringify this and all child nodes using flow rather than
+   * block styles.
+   */
+  declare flow?: boolean
 
   constructor(type: symbol, schema?: Schema) {
     super(type)
@@ -205,8 +210,7 @@ export abstract class Collection extends NodeBase {
     onChompKeep?: () => void
   ) {
     const { indent, indentStep } = ctx
-    const inFlow =
-      this.type === Type.FLOW_MAP || this.type === Type.FLOW_SEQ || ctx.inFlow
+    const inFlow = this.flow || ctx.inFlow
     if (inFlow) itemIndent += indentStep
     ctx = Object.assign({}, ctx, { indent: itemIndent, inFlow, type: null })
     let chompKeep = false
diff --git a/src/nodes/Merge.ts b/src/nodes/Merge.ts
deleted file mode 100644
index eeda08e..0000000
--- a/src/nodes/Merge.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { StringifyContext } from '../stringify/stringify.js'
-import type { Alias } from './Alias.js'
-import { isMap, isNode, isPair, isSeq } from './Node.js'
-import { Pair, PairType } from './Pair.js'
-import { Scalar } from './Scalar.js'
-import type { ToJSContext } from './toJS.js'
-import { YAMLSeq } from './YAMLSeq.js'
-
-export class Merge extends Pair<Scalar, YAMLSeq<Alias>> {
-  static KEY = '<<'
-
-  type: PairType.MERGE_PAIR
-
-  declare value: YAMLSeq<Alias>
-
-  constructor(pair?: Pair<Scalar, Alias | YAMLSeq<Alias>>) {
-    if (isPair(pair) && isNode(pair.value)) {
-      if (isSeq(pair.value)) super(pair.key, pair.value)
-      else {
-        const seq = new YAMLSeq<Alias>()
-        seq.items.push(pair.value)
-        seq.range = pair.value.range
-        super(pair.key, seq)
-      }
-      this.range = pair.range
-    } else {
-      super(new Scalar(Merge.KEY), new YAMLSeq())
-    }
-    this.type = PairType.MERGE_PAIR
-  }
-
-  // If the value associated with a merge key is a single mapping node, each of
-  // its key/value pairs is inserted into the current mapping, unless the key
-  // already exists in it. If the value associated with the merge key is a
-  // sequence, then this sequence is expected to contain mapping nodes and each
-  // of these nodes is merged in turn according to its order in the sequence.
-  // Keys in mapping nodes earlier in the sequence override keys specified in
-  // later mapping nodes. -- http://yaml.org/type/merge.html
-  addToJSMap(
-    ctx: ToJSContext | undefined,
-    map:
-      | Map<unknown, unknown>
-      | Set<unknown>
-      | Record<string | number | symbol, unknown>
-  ) {
-    for (const { source } of this.value.items) {
-      if (!isMap(source)) throw new Error('Merge sources must be maps')
-      const srcMap = source.toJSON(null, ctx, Map)
-      for (const [key, value] of srcMap) {
-        if (map instanceof Map) {
-          if (!map.has(key)) map.set(key, value)
-        } else if (map instanceof Set) {
-          map.add(key)
-        } else if (!Object.prototype.hasOwnProperty.call(map, key)) {
-          Object.defineProperty(map, key, {
-            value,
-            writable: true,
-            enumerable: true,
-            configurable: true
-          })
-        }
-      }
-    }
-    return map
-  }
-
-  toString(ctx?: StringifyContext, onComment?: () => void) {
-    const seq = this.value
-    if (seq.items.length > 1) return super.toString(ctx, onComment)
-    try {
-      this.value = seq.items[0] as any
-      return super.toString(ctx, onComment)
-    } finally {
-      this.value = seq
-    }
-  }
-}
diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts
index 6d33ea6..e5d72a5 100644
--- a/src/nodes/Node.ts
+++ b/src/nodes/Node.ts
@@ -1,8 +1,7 @@
-import type { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
 import type { StringifyContext } from '../stringify/stringify.js'
 import type { Alias } from './Alias.js'
-import type { Pair, PairType } from './Pair.js'
+import type { Pair } from './Pair.js'
 import type { Scalar } from './Scalar.js'
 import type { YAMLMap } from './YAMLMap.js'
 import type { YAMLSeq } from './YAMLSeq.js'
@@ -93,9 +92,6 @@ export abstract class NodeBase {
     onChompKeep?: () => void
   ): string
 
-  /** The type of this node */
-  declare type?: Type | PairType
-
   constructor(type: symbol) {
     Object.defineProperty(this, NODE_TYPE, { value: type })
   }
diff --git a/src/nodes/Pair.ts b/src/nodes/Pair.ts
index 6857d9a..6bc6107 100644
--- a/src/nodes/Pair.ts
+++ b/src/nodes/Pair.ts
@@ -1,4 +1,3 @@
-import { Type } from '../constants.js'
 import { createNode, CreateNodeContext } from '../doc/createNode.js'
 import { warn } from '../log.js'
 import { addComment } from '../stringify/addComment.js'
@@ -11,7 +10,9 @@ import {
 import { Scalar } from './Scalar.js'
 import { toJS, ToJSContext } from './toJS.js'
 import {
+  isAlias,
   isCollection,
+  isMap,
   isNode,
   isScalar,
   isSeq,
@@ -30,25 +31,60 @@ export function createPair(
   return new Pair(k, v)
 }
 
-export enum PairType {
-  PAIR = 'PAIR',
-  MERGE_PAIR = 'MERGE_PAIR'
+const isMergeKey = (key: unknown) =>
+  key === Pair.MERGE_KEY ||
+  (isScalar(key) &&
+    key.value === Pair.MERGE_KEY &&
+    (!key.type || key.type === Scalar.PLAIN))
+
+// If the value associated with a merge key is a single mapping node, each of
+// its key/value pairs is inserted into the current mapping, unless the key
+// already exists in it. If the value associated with the merge key is a
+// sequence, then this sequence is expected to contain mapping nodes and each
+// of these nodes is merged in turn according to its order in the sequence.
+// Keys in mapping nodes earlier in the sequence override keys specified in
+// later mapping nodes. -- http://yaml.org/type/merge.html
+function mergeToJSMap(
+  ctx: ToJSContext | undefined,
+  map:
+    | Map<unknown, unknown>
+    | Set<unknown>
+    | Record<string | number | symbol, unknown>,
+  value: unknown
+) {
+  if (!isAlias(value) || !isMap(value.source))
+    throw new Error('Merge sources must be map aliases')
+  const srcMap = value.source.toJSON(null, ctx, Map)
+  for (const [key, value] of srcMap) {
+    if (map instanceof Map) {
+      if (!map.has(key)) map.set(key, value)
+    } else if (map instanceof Set) {
+      map.add(key)
+    } else if (!Object.prototype.hasOwnProperty.call(map, key)) {
+      Object.defineProperty(map, key, {
+        value,
+        writable: true,
+        enumerable: true,
+        configurable: true
+      })
+    }
+  }
+  return map
 }
 
 export class Pair<K = unknown, V = unknown> extends NodeBase {
+  static readonly MERGE_KEY = '<<'
+
   /** Always Node or null when parsed, but can be set to anything. */
   key: K
 
   /** Always Node or null when parsed, but can be set to anything. */
   value: V | null
 
-  type: PairType
-
   constructor(key: K, value: V | null = null) {
     super(PAIR)
     this.key = key
     this.value = value
-    this.type = PairType.PAIR
   }
 
   // @ts-ignore This is fine.
@@ -88,23 +124,31 @@ export class Pair<K = unknown, V = unknown> extends NodeBase {
       | Set<unknown>
       | Record<string | number | symbol, unknown>
   ) {
-    const key = toJS(this.key, '', ctx)
-    if (map instanceof Map) {
-      const value = toJS(this.value, key, ctx)
-      map.set(key, value)
-    } else if (map instanceof Set) {
-      map.add(key)
+    if (ctx && ctx.doc.schema.merge && isMergeKey(this.key)) {
+      if (isSeq(this.value))
+        for (const it of this.value.items) mergeToJSMap(ctx, map, it)
+      else if (Array.isArray(this.value))
+        for (const it of this.value) mergeToJSMap(ctx, map, it)
+      else mergeToJSMap(ctx, map, this.value)
     } else {
-      const stringKey = stringifyKey(this.key, key, ctx)
-      const value = toJS(this.value, stringKey, ctx)
-      if (stringKey in map)
-        Object.defineProperty(map, stringKey, {
-          value,
-          writable: true,
-          enumerable: true,
-          configurable: true
-        })
-      else map[stringKey] = value
+      const key = toJS(this.key, '', ctx)
+      if (map instanceof Map) {
+        const value = toJS(this.value, key, ctx)
+        map.set(key, value)
+      } else if (map instanceof Set) {
+        map.add(key)
+      } else {
+        const stringKey = stringifyKey(this.key, key, ctx)
+        const value = toJS(this.value, stringKey, ctx)
+        if (stringKey in map)
+          Object.defineProperty(map, stringKey, {
+            value,
+            writable: true,
+            enumerable: true,
+            configurable: true
+          })
+        else map[stringKey] = value
+      }
     }
     return map
   }
@@ -144,7 +188,7 @@ export class Pair<K = unknown, V = unknown> extends NodeBase {
         (keyComment && value == null) ||
         isCollection(key) ||
         (isScalar(key)
-          ? key.type === Type.BLOCK_FOLDED || key.type === Type.BLOCK_LITERAL
+          ? key.type === Scalar.BLOCK_FOLDED || key.type === Scalar.BLOCK_LITERAL
           : typeof key === 'object'))
 
     ctx = Object.assign({}, ctx, {
@@ -214,7 +258,7 @@ export class Pair<K = unknown, V = unknown> extends NodeBase {
       !ctx.inFlow &&
       !explicitKey &&
       isSeq(value) &&
-      value.type !== Type.FLOW_SEQ &&
+      !value.flow &&
       !value.tag &&
       !doc.anchors.getName(value)
     ) {
diff --git a/src/nodes/Scalar.ts b/src/nodes/Scalar.ts
index 6c7c66c..e731c30 100644
--- a/src/nodes/Scalar.ts
+++ b/src/nodes/Scalar.ts
@@ -1,4 +1,3 @@
-import type { Type } from '../constants.js'
 import { NodeBase, SCALAR } from './Node.js'
 import { toJS, ToJSContext } from './toJS.js'
 
@@ -11,15 +10,22 @@ export declare namespace Scalar {
     source: string
   }
 
-  type Type =
-    | Type.BLOCK_FOLDED
-    | Type.BLOCK_LITERAL
-    | Type.PLAIN
-    | Type.QUOTE_DOUBLE
-    | Type.QUOTE_SINGLE
+  type BLOCK_FOLDED = 'BLOCK_FOLDED'
+  type BLOCK_LITERAL = 'BLOCK_LITERAL'
+  type PLAIN = 'PLAIN'
+  type QUOTE_DOUBLE = 'QUOTE_DOUBLE'
+  type QUOTE_SINGLE = 'QUOTE_SINGLE'
+
+  type Type = BLOCK_FOLDED | BLOCK_LITERAL | PLAIN | QUOTE_DOUBLE | QUOTE_SINGLE
 }
 
 export class Scalar<T = unknown> extends NodeBase {
+  static readonly BLOCK_FOLDED = 'BLOCK_FOLDED'
+  static readonly BLOCK_LITERAL = 'BLOCK_LITERAL'
+  static readonly PLAIN = 'PLAIN'
+  static readonly QUOTE_DOUBLE = 'QUOTE_DOUBLE'
+  static readonly QUOTE_SINGLE = 'QUOTE_SINGLE'
+
   value: T
 
   declare type?: Scalar.Type
diff --git a/src/nodes/YAMLMap.ts b/src/nodes/YAMLMap.ts
index ccffbec..4a2955b 100644
--- a/src/nodes/YAMLMap.ts
+++ b/src/nodes/YAMLMap.ts
@@ -1,4 +1,3 @@
-import type { Type } from '../constants.js'
 import type { Schema } from '../doc/Schema.js'
 import type { StringifyContext } from '../stringify/stringify.js'
 import { Collection } from './Collection.js'
@@ -38,8 +37,6 @@ export class YAMLMap<K = unknown, V = unknown> extends Collection {
 
   items: Pair<K, V>[] = []
 
-  type?: Type.FLOW_MAP | Type.MAP
-
   constructor(schema?: Schema) {
     super(MAP, schema)
   }
diff --git a/src/nodes/YAMLSeq.ts b/src/nodes/YAMLSeq.ts
index a48a97c..b345150 100644
--- a/src/nodes/YAMLSeq.ts
+++ b/src/nodes/YAMLSeq.ts
@@ -1,4 +1,3 @@
-import type { Type } from '../constants.js'
 import type { Schema } from '../doc/Schema.js'
 import type { StringifyContext } from '../stringify/stringify.js'
 import { Collection } from './Collection.js'
@@ -23,8 +22,6 @@ export class YAMLSeq<T = unknown> extends Collection {
 
   items: T[] = []
 
-  type?: Type.FLOW_SEQ | Type.SEQ
-
   constructor(schema?: Schema) {
     super(SEQ, schema)
   }
diff --git a/src/options.ts b/src/options.ts
index 70aff82..0ec1ead 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -1,8 +1,8 @@
-import type { LogLevelId } from './constants.js'
 import type { Reviver } from './doc/applyReviver.js'
 import type { Directives } from './doc/directives.js'
 import type { Replacer } from './doc/Document.js'
 import type { SchemaName } from './doc/Schema.js'
+import type { LogLevelId } from './log.js'
 import type { Pair } from './nodes/Pair.js'
 import type { Scalar } from './nodes/Scalar.js'
 import type { LineCounter } from './parse/line-counter.js'
diff --git a/src/public-api.ts b/src/public-api.ts
index a33beab..a740731 100644
--- a/src/public-api.ts
+++ b/src/public-api.ts
@@ -1,5 +1,4 @@
 import { Composer } from './compose/composer.js'
-import { LogLevel } from './constants.js'
 import type { Reviver } from './doc/applyReviver.js'
 import { Document, Replacer } from './doc/Document.js'
 import { YAMLParseError } from './errors.js'
@@ -52,7 +51,7 @@ export function parseDocument<T extends ParsedNode = ParsedNode>(
   let doc: Document.Parsed<T> | null = null
   const composer = new Composer(_doc => {
     if (!doc) doc = _doc as Document.Parsed<T>
-    else if (LogLevel.indexOf(doc.options.logLevel) >= LogLevel.ERROR) {
+    else if (doc.options.logLevel !== 'silent') {
       const errMsg =
         'Source contains multiple documents; please use YAML.parseAllDocuments()'
       doc.errors.push(new YAMLParseError(_doc.range[0], errMsg))
@@ -101,8 +100,7 @@ export function parse(
   if (!doc) return null
   doc.warnings.forEach(warning => warn(doc.options.logLevel, warning))
   if (doc.errors.length > 0) {
-    if (LogLevel.indexOf(doc.options.logLevel) >= LogLevel.ERROR)
-      throw doc.errors[0]
+    if (doc.options.logLevel !== 'silent') throw doc.errors[0]
     else doc.errors = []
   }
   return doc.toJS(Object.assign({ reviver: _reviver }, options))
@@ -114,16 +112,19 @@ export function parse(
  * @param replacer - A replacer array or function, as in `JSON.stringify()`
  * @returns Will always include `\n` as the last character, as is expected of YAML documents.
  */
-export function stringify(value: any, options?: Options & ToStringOptions): string
+export function stringify(
+  value: any,
+  options?: Options & ToStringOptions
+): string
 export function stringify(
   value: any,
   replacer?: Replacer | null,
-  options?: string | number | Options & ToStringOptions
+  options?: string | number | (Options & ToStringOptions)
 ): string
 export function stringify(
   value: any,
-  replacer?: Replacer | Options & ToStringOptions | null,
-  options?: string | number | Options & ToStringOptions
+  replacer?: Replacer | (Options & ToStringOptions) | null,
+  options?: string | number | (Options & ToStringOptions)
 ) {
   let _replacer: Replacer | null = null
   if (typeof replacer === 'function' || Array.isArray(replacer)) {
@@ -138,7 +139,8 @@ export function stringify(
     options = indent < 1 ? undefined : indent > 8 ? { indent: 8 } : { indent }
   }
   if (value === undefined) {
-    const { keepUndefined } = options || (replacer as Options & ToStringOptions) || {}
+    const { keepUndefined } =
+      options || (replacer as Options & ToStringOptions) || {}
     if (!keepUndefined) return undefined
   }
   return new Document(value, _replacer, options).toString(options)
diff --git a/src/stringify/stringify.ts b/src/stringify/stringify.ts
index 8dedf50..273b0d9 100644
--- a/src/stringify/stringify.ts
+++ b/src/stringify/stringify.ts
@@ -1,4 +1,3 @@
-import { Type } from '../constants.js'
 import type { Document } from '../doc/Document.js'
 import { isAlias, isNode, isPair, isScalar, Node } from '../nodes/Node.js'
 import type { Scalar } from '../nodes/Scalar.js'
@@ -33,7 +32,7 @@ export const createStringifyContext = (
   options: Object.assign(
     {
       defaultKeyType: null,
-      defaultStringType: Type.PLAIN,
+      defaultStringType: 'PLAIN',
       directives: null,
       doubleQuotedAsJSON: false,
       doubleQuotedMinMultiLineLength: 40,
diff --git a/src/stringify/stringifyString.ts b/src/stringify/stringifyString.ts
index 6b0a847..532cd43 100644
--- a/src/stringify/stringifyString.ts
+++ b/src/stringify/stringifyString.ts
@@ -1,5 +1,4 @@
-import { Type } from '../constants.js'
-import type { Scalar } from '../nodes/Scalar.js'
+import { Scalar } from '../nodes/Scalar.js'
 import { addCommentBefore } from './addComment.js'
 import {
   foldFlowLines,
@@ -161,9 +160,9 @@ function blockString(
     (ctx.forceBlockIndent || containsDocumentMarker(value) ? '  ' : '')
   const indentSize = indent ? '2' : '1' // root is at -1
   const literal =
-    type === Type.BLOCK_FOLDED
+    type === Scalar.BLOCK_FOLDED
       ? false
-      : type === Type.BLOCK_LITERAL
+      : type === Scalar.BLOCK_LITERAL
       ? true
       : !lineLengthOverLimit(value, ctx.options.lineWidth, indent.length)
   let header = literal ? '|' : '>'
@@ -263,7 +262,7 @@ function plainString(
   if (
     !implicitKey &&
     !inFlow &&
-    type !== Type.PLAIN &&
+    type !== Scalar.PLAIN &&
     value.indexOf('\n') !== -1
   ) {
     // Where allowed & type not set explicitly, prefer block style for multiline strings
@@ -314,24 +313,24 @@ export function stringifyString(
       : Object.assign({}, item, { value: String(item.value) })
 
   let { type } = item
-  if (type !== Type.QUOTE_DOUBLE) {
+  if (type !== Scalar.QUOTE_DOUBLE) {
     // force double quotes on control characters & unpaired surrogates
     if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value))
-      type = Type.QUOTE_DOUBLE
+      type = Scalar.QUOTE_DOUBLE
   }
 
-  const _stringify = (_type: Type | undefined) => {
+  const _stringify = (_type: Scalar.Type | undefined) => {
     switch (_type) {
-      case Type.BLOCK_FOLDED:
-      case Type.BLOCK_LITERAL:
+      case Scalar.BLOCK_FOLDED:
+      case Scalar.BLOCK_LITERAL:
         return implicitKey || inFlow
           ? doubleQuotedString(ss.value, ctx) // blocks are not valid inside flow containers
           : blockString(ss, ctx, onComment, onChompKeep)
-      case Type.QUOTE_DOUBLE:
+      case Scalar.QUOTE_DOUBLE:
         return doubleQuotedString(ss.value, ctx)
-      case Type.QUOTE_SINGLE:
+      case Scalar.QUOTE_SINGLE:
         return singleQuotedString(ss.value, ctx)
-      case Type.PLAIN:
+      case Scalar.PLAIN:
         return plainString(ss, ctx, onComment, onChompKeep)
       default:
         return null
diff --git a/src/tags/yaml-1.1/binary.ts b/src/tags/yaml-1.1/binary.ts
index 53d6c88..1d40ddf 100644
--- a/src/tags/yaml-1.1/binary.ts
+++ b/src/tags/yaml-1.1/binary.ts
@@ -1,5 +1,4 @@
-import { Type } from '../../constants.js'
-import type { Scalar } from '../../nodes/Scalar.js'
+import { Scalar } from '../../nodes/Scalar.js'
 import { stringifyString } from '../../stringify/stringifyString.js'
 import type { ScalarTag } from '../types.js'
 
@@ -51,8 +50,8 @@ export const binary: ScalarTag = {
       )
     }
 
-    if (!type) type = Type.BLOCK_LITERAL
-    if (type !== Type.QUOTE_DOUBLE) {
+    if (!type) type = Scalar.BLOCK_LITERAL
+    if (type !== Scalar.QUOTE_DOUBLE) {
       const lineWidth = Math.max(
         ctx.options.lineWidth - ctx.indent.length,
         ctx.options.minContentWidth
@@ -62,7 +61,7 @@ export const binary: ScalarTag = {
       for (let i = 0, o = 0; i < n; ++i, o += lineWidth) {
         lines[i] = str.substr(o, lineWidth)
       }
-      str = lines.join(type === Type.BLOCK_LITERAL ? '\n' : ' ')
+      str = lines.join(type === Scalar.BLOCK_LITERAL ? '\n' : ' ')
     }
 
     return stringifyString(
diff --git a/src/util.ts b/src/util.ts
index cf6c984..3bbcf56 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,5 +1,4 @@
-export type { LogLevelId } from './constants.js'
-export { debug, warn } from './log.js'
+export { debug, LogLevelId, warn } from './log.js'
 export { findPair } from './nodes/YAMLMap.js'
 export { toJS, ToJSAnchorValue, ToJSContext } from './nodes/toJS.js'
 export { foldFlowLines } from './stringify/foldFlowLines'
diff --git a/src/visit.ts b/src/visit.ts
index 0606d15..f314093 100644
--- a/src/visit.ts
+++ b/src/visit.ts
@@ -124,7 +124,7 @@ function _visit(
     } else if (isDocument(parent)) {
       parent.contents = ctrl
     } else {
-      const pt = parent && parent.type
+      const pt = isAlias(parent) ? 'alias' : 'scalar'
       throw new Error(`Cannot replace node with ${pt} parent`)
     }
     return _visit(key, ctrl, visitor, path)


Test patch:
diff --git a/src/test-events.ts b/src/test-events.ts
index 2ee2684..df816a3 100644
--- a/src/test-events.ts
+++ b/src/test-events.ts
@@ -1,10 +1,25 @@
 import { Document } from './doc/Document.js'
-import { isNode, ParsedNode } from './nodes/Node.js'
+import {
+  isAlias,
+  isMap,
+  isNode,
+  isPair,
+  isSeq,
+  ParsedNode
+} from './nodes/Node.js'
 import { Pair } from './nodes/Pair.js'
 import { Scalar } from './nodes/Scalar.js'
 import type { Options } from './options.js'
 import { parseAllDocuments } from './public-api.js'
 
+const scalarChar: Record<string, string> = {
+  BLOCK_FOLDED: '>',
+  BLOCK_LITERAL: '|',
+  PLAIN: ':',
+  QUOTE_DOUBLE: '"',
+  QUOTE_SINGLE: "'"
+}
+
 // test harness for yaml-test-suite event tests
 export function testEvents(src: string, options?: Options) {
   const opt = Object.assign({ version: '1.2' }, options)
@@ -64,60 +79,35 @@ function addEvents(
     props = ` &${anchor}`
   }
   if (node.tag) props += ` <${node.tag}>`
-  let scalar = null
-  switch (node.type) {
-    case 'ALIAS':
-      {
-        let alias = doc.anchors.getName(node.source)
-        if (alias && /\d$/.test(alias)) {
-          const alt = alias.replace(/\d$/, '')
-          if (doc.anchors.getNode(alt)) alias = alt
-        }
-        events.push(`=ALI${props} *${alias}`)
-      }
-      break
-    case 'BLOCK_FOLDED':
-      scalar = '>'
-      break
-    case 'BLOCK_LITERAL':
-      scalar = '|'
-      break
-    case 'PLAIN':
-      scalar = ':'
-      break
-    case 'QUOTE_DOUBLE':
-      scalar = '"'
-      break
-    case 'QUOTE_SINGLE':
-      scalar = "'"
-      break
-    case 'PAIR':
-      events.push(`+MAP${props}`)
-      addEvents(events, doc, errPos, node.key)
-      addEvents(events, doc, errPos, node.value)
-      events.push('-MAP')
-      break
-    case 'FLOW_SEQ':
-    case 'SEQ':
-      events.push(`+SEQ${props}`)
-      node.items.forEach(item => {
-        addEvents(events, doc, errPos, item)
-      })
-      events.push('-SEQ')
-      break
-    case 'FLOW_MAP':
-    case 'MAP':
-      events.push(`+MAP${props}`)
-      node.items.forEach(({ key, value }) => {
-        addEvents(events, doc, errPos, key)
-        addEvents(events, doc, errPos, value)
-      })
-      events.push('-MAP')
-      break
-    default:
-      throw new Error(`Unexpected node type ${node.type}`)
-  }
-  if (scalar) {
+
+  if (isMap(node)) {
+    events.push(`+MAP${props}`)
+    node.items.forEach(({ key, value }) => {
+      addEvents(events, doc, errPos, key)
+      addEvents(events, doc, errPos, value)
+    })
+    events.push('-MAP')
+  } else if (isSeq(node)) {
+    events.push(`+SEQ${props}`)
+    node.items.forEach(item => {
+      addEvents(events, doc, errPos, item)
+    })
+    events.push('-SEQ')
+  } else if (isPair(node)) {
+    events.push(`+MAP${props}`)
+    addEvents(events, doc, errPos, node.key)
+    addEvents(events, doc, errPos, node.value)
+    events.push('-MAP')
+  } else if (isAlias(node)) {
+    let alias = doc.anchors.getName(node.source)
+    if (alias && /\d$/.test(alias)) {
+      const alt = alias.replace(/\d$/, '')
+      if (doc.anchors.getNode(alt)) alias = alt
+    }
+    events.push(`=ALI${props} *${alias}`)
+  } else {
+    const scalar = scalarChar[String(node.type)]
+    if (!scalar) throw new Error(`Unexpected node type ${node.type}`)
     const value = (node as Scalar.Parsed).source
       .replace(/\\/g, '\\\\')
       .replace(/\0/g, '\\0')
diff --git a/tests/doc/anchors.js b/tests/doc/anchors.js
index 11c60f7..729ee75 100644
--- a/tests/doc/anchors.js
+++ b/tests/doc/anchors.js
@@ -161,16 +161,18 @@ describe('merge <<', () => {
     ])
     doc.contents.items.slice(5).forEach(({ items }) => {
       const merge = items[0]
-      expect(merge).toBeInstanceOf(YAML.Merge)
-      merge.value.items.forEach(({ source }) => {
-        expect(source).toBeInstanceOf(YAML.YAMLMap)
-      })
+      expect(YAML.isPair(merge)).toBe(true)
+      if (YAML.isSeq(merge.value))
+        merge.value.items.forEach(({ source }) => {
+          expect(source).toBeInstanceOf(YAML.YAMLMap)
+        })
+      else expect(merge.value.source).toBeInstanceOf(YAML.YAMLMap)
     })
   })
 
   describe('doc.anchors.createMergePair', () => {
     test('simple case', () => {
-      const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
+      const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
       const [a, b] = doc.contents.items
       const merge = doc.anchors.createMergePair(a)
       b.items.push(merge)
@@ -179,7 +181,7 @@ describe('merge <<', () => {
     })
 
     test('merge pair of an alias', () => {
-      const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
+      const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
       const [a, b] = doc.contents.items
       const alias = doc.anchors.createAlias(a, 'AA')
       const merge = doc.anchors.createMergePair(alias)
@@ -190,7 +192,7 @@ describe('merge <<', () => {
 
     test('require map node', () => {
       const exp = 'Merge sources must be Map nodes or their Aliases'
-      const doc = YAML.parseDocument('[{ a: A }, { b: B }]')
+      const doc = YAML.parseDocument('[{ a: A }, { b: B }]', { merge: true })
       const [a] = doc.contents.items
       const merge = doc.anchors.createMergePair(a)
       expect(() => doc.anchors.createMergePair(merge)).toThrow(exp)
@@ -287,14 +289,14 @@ y:
     test('non-alias merge value', () => {
       const src = '{ <<: A, B: b }'
       expect(() => YAML.parse(src, { merge: true })).toThrow(
-        'Merge nodes can only have Alias nodes as values'
+        'Merge sources must be map aliases'
       )
     })
 
     test('non-map alias', () => {
       const src = '- &A a\n- { <<: *A, B: b }'
       expect(() => YAML.parse(src, { merge: true })).toThrow(
-        'Merge nodes aliases can only point to maps'
+        'Merge sources must be map aliases'
       )
     })
 
diff --git a/tests/doc/createNode.js b/tests/doc/createNode.js
index f11bf22..5eecede 100644
--- a/tests/doc/createNode.js
+++ b/tests/doc/createNode.js
@@ -1,4 +1,4 @@
-import { Document, PairType, Scalar, YAMLMap, YAMLSeq } from 'yaml'
+import { Document, Scalar, YAMLMap, YAMLSeq } from 'yaml'
 import { YAMLSet } from '../../src/tags/yaml-1.1/set.js'
 
 let doc
@@ -126,7 +126,7 @@ describe('objects', () => {
     const s = doc.createNode({ x: true, y: undefined })
     expect(s).toBeInstanceOf(YAMLMap)
     expect(s.items).toMatchObject([
-      { type: PairType.PAIR, key: { value: 'x' }, value: { value: true } }
+      { key: { value: 'x' }, value: { value: true } }
     ])
   })
 
@@ -134,8 +134,8 @@ describe('objects', () => {
     const s = doc.createNode({ x: true, y: undefined }, { keepUndefined: true })
     expect(s).toBeInstanceOf(YAMLMap)
     expect(s.items).toMatchObject([
-      { type: PairType.PAIR, key: { value: 'x' }, value: { value: true } },
-      { type: PairType.PAIR, key: { value: 'y' }, value: { value: null } }
+      { key: { value: 'x' }, value: { value: true } },
+      { key: { value: 'y' }, value: { value: null } }
     ])
   })
 
@@ -308,7 +308,6 @@ describe('circular references', () => {
         {
           key: { value: 'map' },
           value: {
-            type: 'ALIAS',
             source: {
               items: [{ key: { value: 'foo' } }, { key: { value: 'map' } }]
             }
@@ -328,7 +327,6 @@ describe('circular references', () => {
     const doc = new Document(null)
     const node = doc.createNode(map)
     expect(node.getIn(['foo', 'bar', 'baz', 'map'])).toMatchObject({
-      type: 'ALIAS',
       source: { items: [{ key: { value: 'foo' } }] }
     })
     expect(doc.anchors.map).toMatchObject({
@@ -345,9 +343,9 @@ describe('circular references', () => {
       items: [
         { items: [{ value: 'one' }] },
         { items: [{ value: 'two' }] },
-        { type: 'ALIAS', source: { items: [{ value: 'one' }] } },
-        { type: 'ALIAS', source: { items: [{ value: 'one' }] } },
-        { type: 'ALIAS', source: { items: [{ value: 'two' }] } }
+        { source: { items: [{ value: 'one' }] } },
+        { source: { items: [{ value: 'one' }] } },
+        { source: { items: [{ value: 'two' }] } }
       ]
     })
     expect(doc.anchors.map).toMatchObject({
@@ -366,7 +364,6 @@ describe('circular references', () => {
     expect(source).toMatchObject({
       items: [{ key: { value: 'a' }, value: { value: 1 } }]
     })
-    expect(alias).toMatchObject({ type: 'ALIAS' })
     expect(alias.source).toBe(source)
   })
 })
diff --git a/tests/doc/errors.js b/tests/doc/errors.js
index 931c959..b8f8ab7 100644
--- a/tests/doc/errors.js
+++ b/tests/doc/errors.js
@@ -81,7 +81,6 @@ describe('block collections', () => {
       { message: 'All mapping items must start at the same column' }
     ])
     expect(doc.contents).toMatchObject({
-      type: 'MAP',
       items: [
         { key: { value: 'foo' }, value: { value: '1' } },
         { key: { value: 'bar' }, value: { value: 2 } }
@@ -96,7 +95,6 @@ describe('block collections', () => {
       { message: 'All sequence items must start at the same column' }
     ])
     expect(doc.contents).toMatchObject({
-      type: 'SEQ',
       items: [{ value: 'foo' }, { items: [{ value: 'bar' }] }]
     })
   })
@@ -110,7 +108,6 @@ describe('block collections', () => {
       { message: 'Implicit map keys need to be followed by map values' }
     ])
     expect(doc.contents).toMatchObject({
-      type: 'MAP',
       items: [
         { key: { value: 'foo' }, value: { value: '1' } },
         { key: { items: [{ value: 'bar' }] }, value: null }
diff --git a/tests/doc/stringify.js b/tests/doc/stringify.js
index 306681d..5a33d6e 100644
--- a/tests/doc/stringify.js
+++ b/tests/doc/stringify.js
@@ -2,7 +2,7 @@
 
 import { source } from 'common-tags'
 import * as YAML from 'yaml'
-import { Pair, Scalar, Type } from 'yaml'
+import { Pair, Scalar } from 'yaml'
 import { stringifyString } from 'yaml/util'
 
 for (const [name, version] of [
@@ -142,7 +142,7 @@ blah blah\n`)
         const foo = 'fuzz'.repeat(16)
         const doc = new YAML.Document({ foo }, version)
         for (const node of doc.contents.items)
-          node.value.type = Type.QUOTE_DOUBLE
+          node.value.type = Scalar.QUOTE_DOUBLE
         expect(
           doc
             .toString(opt)
@@ -154,7 +154,7 @@ blah blah\n`)
       test('long line in sequence', () => {
         const foo = 'fuzz'.repeat(16)
         const doc = new YAML.Document([foo], version)
-        for (const node of doc.contents.items) node.type = Type.QUOTE_DOUBLE
+        for (const node of doc.contents.items) node.type = Scalar.QUOTE_DOUBLE
         expect(
           doc
             .toString(opt)
@@ -167,7 +167,7 @@ blah blah\n`)
         const foo = 'fuzz'.repeat(16)
         const doc = new YAML.Document({ foo: [foo] }, version)
         const seq = doc.contents.items[0].value
-        for (const node of seq.items) node.type = Type.QUOTE_DOUBLE
+        for (const node of seq.items) node.type = Scalar.QUOTE_DOUBLE
         expect(
           doc
             .toString(opt)
@@ -764,43 +764,49 @@ describe('indentSeq: false', () => {
 describe('Scalar options', () => {
   describe('defaultStringType & defaultKeyType', () => {
     test('PLAIN, PLAIN', () => {
-      const opt = { defaultStringType: Type.PLAIN, defaultKeyType: Type.PLAIN }
+      const opt = {
+        defaultStringType: Scalar.PLAIN,
+        defaultKeyType: Scalar.PLAIN
+      }
       expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('foo: bar\n')
     })
 
     test('BLOCK_FOLDED, BLOCK_FOLDED', () => {
       const opt = {
-        defaultStringType: Type.BLOCK_FOLDED,
-        defaultKeyType: Type.BLOCK_FOLDED
+        defaultStringType: Scalar.BLOCK_FOLDED,
+        defaultKeyType: Scalar.BLOCK_FOLDED
       }
       expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('"foo": |-\n  bar\n')
     })
 
     test('QUOTE_DOUBLE, PLAIN', () => {
       const opt = {
-        defaultStringType: Type.QUOTE_DOUBLE,
-        defaultKeyType: Type.PLAIN
+        defaultStringType: Scalar.QUOTE_DOUBLE,
+        defaultKeyType: Scalar.PLAIN
       }
       expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('foo: "bar"\n')
     })
 
     test('QUOTE_DOUBLE, QUOTE_SINGLE', () => {
       const opt = {
-        defaultStringType: Type.QUOTE_DOUBLE,
-        defaultKeyType: Type.QUOTE_SINGLE
+        defaultStringType: Scalar.QUOTE_DOUBLE,
+        defaultKeyType: Scalar.QUOTE_SINGLE
       }
       expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('\'foo\': "bar"\n')
     })
 
     test('QUOTE_DOUBLE, null', () => {
-      const opt = { defaultStringType: Type.QUOTE_DOUBLE, defaultKeyType: null }
+      const opt = {
+        defaultStringType: Scalar.QUOTE_DOUBLE,
+        defaultKeyType: null
+      }
       expect(YAML.stringify({ foo: 'bar' }, opt)).toBe('"foo": "bar"\n')
     })
 
     test('Use defaultType for explicit keys', () => {
       const opt = {
-        defaultStringType: Type.QUOTE_DOUBLE,
-        defaultKeyType: Type.QUOTE_SINGLE
+        defaultStringType: Scalar.QUOTE_DOUBLE,
+        defaultKeyType: Scalar.QUOTE_SINGLE
       }
       const doc = new YAML.Document({ foo: null })
       doc.contents.items[0].value = null
@@ -856,7 +862,7 @@ describe('Document markers in top-level scalars', () => {
 
   test("'foo\\n...'", () => {
     const doc = new YAML.Document('foo\n...')
-    doc.contents.type = Type.QUOTE_SINGLE
+    doc.contents.type = Scalar.QUOTE_SINGLE
     const str = String(doc)
     expect(str).toBe("'foo\n\n  ...'\n")
     expect(YAML.parse(str)).toBe('foo\n...')
@@ -864,7 +870,7 @@ describe('Document markers in top-level scalars', () => {
 
   test('"foo\\n..."', () => {
     const doc = new YAML.Document('foo\n...')
-    doc.contents.type = Type.QUOTE_DOUBLE
+    doc.contents.type = Scalar.QUOTE_DOUBLE
     const str = doc.toString({ doubleQuotedMinMultiLineLength: 0 })
     expect(str).toBe('"foo\n\n  ..."\n')
     expect(YAML.parse(str)).toBe('foo\n...')
diff --git a/tests/doc/types.js b/tests/doc/types.js
index bdcf3e0..b5041b1 100644
--- a/tests/doc/types.js
+++ b/tests/doc/types.js
@@ -553,6 +553,54 @@ date (00:00:00Z): 2002-12-14\n`)
       expect(str).toBe(`set:\n  !!set\n  ? a\n  ? b\n  ? c\n`)
     })
   })
+
+  describe('!!merge', () => {
+    test('alias', () => {
+      const src = '- &a { a: A, b: B }\n- { <<: *a, b: X }\n'
+      const doc = YAML.parseDocument(src, { version: '1.1' })
+      expect(doc.toJS()).toMatchObject([
+        { a: 'A', b: 'B' },
+        { a: 'A', b: 'X' }
+      ])
+      expect(doc.toString()).toBe(src)
+    })
+
+    test('alias sequence', () => {
+      const src =
+        '- &a { a: A, b: B, c: C }\n- &b { a: X }\n- { <<: [ *b, *a ], b: X }\n'
+      const doc = YAML.parseDocument(src, { version: '1.1' })
+      expect(doc.toJS()).toMatchObject([
+        { a: 'A', b: 'B' },
+        { a: 'X' },
+        { a: 'X', b: 'X', c: 'C' }
+      ])
+      expect(doc.toString()).toBe(src)
+    })
+
+    test('explicit creation', () => {
+      const src = '- { a: A, b: B }\n- { b: X }\n'
+      const doc = YAML.parseDocument(src, { version: '1.1' })
+      const alias = doc.anchors.createAlias(doc.get(0), 'a')
+      doc.addIn([1], doc.anchors.createMergePair(alias))
+      expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n')
+      expect(doc.toJS()).toMatchObject([
+        { a: 'A', b: 'B' },
+        { a: 'A', b: 'X' }
+      ])
+    })
+
+    test('creation by duck typing', () => {
+      const src = '- { a: A, b: B }\n- { b: X }\n'
+      const doc = YAML.parseDocument(src, { version: '1.1' })
+      const alias = doc.anchors.createAlias(doc.get(0), 'a')
+      doc.addIn([1], doc.createPair('<<', alias))
+      expect(doc.toString()).toBe('- &a { a: A, b: B }\n- { b: X, <<: *a }\n')
+      expect(doc.toJS()).toMatchObject([
+        { a: 'A', b: 'B' },
+        { a: 'A', b: 'X' }
+      ])
+    })
+  })
 })
 
 describe('custom tags', () => {
diff --git a/tests/visit.ts b/tests/visit.ts
index 794ee05..5083732 100644
--- a/tests/visit.ts
+++ b/tests/visit.ts
@@ -1,15 +1,17 @@
-import { Document, Pair, ParsedNode, parseDocument, Scalar, visit } from 'yaml'
+import { Document, isSeq, parseDocument, Scalar, visit } from 'yaml'
+
+const coll = { items: {} }
 
 test('Map', () => {
   const doc = parseDocument('{ one: 1, two }')
   const fn = jest.fn()
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'FLOW_MAP' }, [{}]],
-    [0, { type: 'PAIR' }, [{}, {}]],
+    [null, coll, [{}]],
+    [0, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'one' }, [{}, {}, {}]],
     ['value', { type: 'PLAIN', value: 1 }, [{}, {}, {}]],
-    [1, { type: 'PAIR' }, [{}, {}]],
+    [1, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'two' }, [{}, {}, {}]]
   ])
 })
@@ -19,13 +21,9 @@ test('Seq', () => {
   const fn = jest.fn()
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'SEQ' }, [{ type: 'DOCUMENT' }]],
-    [0, { type: 'PLAIN', value: 1 }, [{ type: 'DOCUMENT' }, { type: 'SEQ' }]],
-    [
-      1,
-      { type: 'PLAIN', value: 'two' },
-      [{ type: 'DOCUMENT' }, { type: 'SEQ' }]
-    ]
+    [null, coll, [{ contents: {} }]],
+    [0, { type: 'PLAIN', value: 1 }, [{ contents: {} }, coll]],
+    [1, { type: 'PLAIN', value: 'two' }, [{ contents: {} }, coll]]
   ])
 })
 
@@ -34,9 +32,9 @@ test('Alias', () => {
   const fn = jest.fn()
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'SEQ' }, [{}]],
+    [null, coll, [{}]],
     [0, { type: 'PLAIN', value: 1 }, [{}, {}]],
-    [1, { type: 'ALIAS', source: { value: 1 } }, [{}, {}]]
+    [1, { source: { value: 1 } }, [{}, {}]]
   ])
 })
 
@@ -45,10 +43,10 @@ test('Seq in Map', () => {
   const fn = jest.fn()
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Alias: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'MAP' }, [{}]],
-    [0, { type: 'PAIR' }, [{}, {}]],
+    [null, coll, [{}]],
+    [0, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'foo' }, [{}, {}, {}]],
-    ['value', { type: 'SEQ' }, [{}, {}, {}]],
+    ['value', coll, [{}, {}, {}]],
     [0, { type: 'QUOTE_DOUBLE', value: 'one' }, [{}, {}, {}, {}]],
     [1, { type: 'QUOTE_SINGLE', value: 'two' }, [{}, {}, {}, {}]]
   ])
@@ -59,9 +57,9 @@ test('Visit called with non-Document root', () => {
   const fn = jest.fn()
   visit(doc.get('foo') as Scalar, fn)
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'SEQ' }, []],
-    [0, { type: 'QUOTE_DOUBLE', value: 'one' }, [{ type: 'SEQ' }]],
-    [1, { type: 'QUOTE_SINGLE', value: 'two' }, [{ type: 'SEQ' }]]
+    [null, coll, []],
+    [0, { type: 'QUOTE_DOUBLE', value: 'one' }, [coll]],
+    [1, { type: 'QUOTE_SINGLE', value: 'two' }, [coll]]
   ])
 })
 
@@ -93,11 +91,11 @@ test('Function as visitor', () => {
   const fn = jest.fn()
   visit(doc, fn)
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'FLOW_MAP' }, [{}]],
-    [0, { type: 'PAIR' }, [{}, {}]],
+    [null, coll, [{}]],
+    [0, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'one' }, [{}, {}, {}]],
     ['value', { type: 'PLAIN', value: 1 }, [{}, {}, {}]],
-    [1, { type: 'PAIR' }, [{}, {}]],
+    [1, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'two' }, [{}, {}, {}]],
     ['value', null, [{}, {}, {}]]
   ])
@@ -143,16 +141,14 @@ test('Add item to Seq', () => {
 
 test('Do not visit block seq items', () => {
   const doc = parseDocument('foo:\n  - one\n  - two\nbar:\n')
-  const fn = jest.fn((_, node) =>
-    node.type === 'SEQ' ? visit.SKIP : undefined
-  )
+  const fn = jest.fn((_, node) => (isSeq(node) ? visit.SKIP : undefined))
   visit(doc, { Map: fn, Pair: fn, Seq: fn, Scalar: fn })
   expect(fn.mock.calls).toMatchObject([
-    [null, { type: 'MAP' }, [{}]],
-    [0, { type: 'PAIR' }, [{}, {}]],
+    [null, coll, [{}]],
+    [0, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'foo' }, [{}, {}, {}]],
-    ['value', { type: 'SEQ' }, [{}, {}, {}]],
-    [1, { type: 'PAIR' }, [{}, {}]],
+    ['value', coll, [{}, {}, {}]],
+    [1, { key: {}, value: {} }, [{}, {}]],
     ['key', { type: 'PLAIN', value: 'bar' }, [{}, {}, {}]],
     ['value', { type: 'PLAIN', value: null }, [{}, {}, {}]]
   ])
@@ -228,6 +224,6 @@ test('Fail to replace root node', () => {
   const doc = parseDocument('- one\n- two\n- three\n')
   const Seq = jest.fn(() => doc.createNode(42))
   expect(() => visit(doc.contents, { Seq })).toThrow(
-    'Cannot replace node with undefined parent'
+    'Cannot replace node with scalar parent'
   )
 })