Skip to content

Item 64: Consider Brands for Nominal Typing

要点

  • With nominal typing, a value has a type because you say it has a type, not because it has the same shape as that type.
  • Consider attaching brands to distinguish primitive and object types that are semantically distinct but structurally identical.
  • Be familiar with the various techniques for branding: properties on object types, string-based enums, private fields, and unique symbols.
  • 在命名类型系统中,一个值之所以有某种类型,是因为你声明它具有该类型,而不是因为它的形状与该类型相同。
  • 考虑附加标记来区分语义上不同但结构上相同的原始类型和对象类型。
  • 熟悉多种标记技术:对象类型的属性、基于字符串的枚举、私有字段和唯一符号等。

正文

ts
interface Vector2D {
  x: number
  y: number
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x ** 2 + p.y ** 2)
}

calculateNorm({ x: 3, y: 4 }) // OK, result is 5
const vec3D = { x: 3, y: 4, z: 1 }
calculateNorm(vec3D) // OK! result is also 5

💻 playground


ts
interface Vector2D {
  type: '2d'
  x: number
  y: number
}

💻 playground


ts
type AbsolutePath = string & { _brand: 'abs' }
function listAbsolutePath(path: AbsolutePath) {
  // ...
}
function isAbsolutePath(path: string): path is AbsolutePath {
  return path.startsWith('/')
}

💻 playground


ts
function f(path: string) {
  if (isAbsolutePath(path)) {
    listAbsolutePath(path)
  }
  listAbsolutePath(path)
  //               ~~~~ Argument of type 'string' is not assignable to
  //                    parameter of type 'AbsolutePath'
}

💻 playground


ts
type Meters = number & { _brand: 'meters' }
type Seconds = number & { _brand: 'seconds' }

const meters = (m: number) => m as Meters
const seconds = (s: number) => s as Seconds

const oneKm = meters(1000)
//    ^? const oneKm: Meters
const oneMin = seconds(60)
//    ^? const oneMin: Seconds

💻 playground


ts
const tenKm = oneKm * 10
//    ^? const tenKm: number
const v = oneKm / oneMin
//    ^? const v: number

💻 playground


ts
declare const brand: unique symbol
export type Meters = number & { [brand]: 'meters' }

💻 playground


ts
function binarySearch<T>(xs: T[], x: T): boolean {
  let low = 0,
    high = xs.length - 1
  while (high >= low) {
    const mid = low + Math.floor((high - low) / 2)
    const v = xs[mid]
    if (v === x) return true
    ;[low, high] = x > v ? [mid + 1, high] : [low, mid - 1]
  }
  return false
}

💻 playground


ts
type SortedList<T> = T[] & { _brand: 'sorted' }

function isSorted<T>(xs: T[]): xs is SortedList<T> {
  for (let i = 0; i < xs.length - 1; i++) {
    if (xs[i] > xs[i + 1]) {
      return false
    }
  }
  return true
}

function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
  // ...
}

💻 playground

Released under the MIT License.