@coderspirit/nominal

中文文档
4.1.1发布于 2024/08/12MITTypeScriptESM + CJS构建来源已验证近 30 天下载 4.7万

Powerful nominal types for your project

原始使用文档
文档目录9 节
  1. Install instructions
  2. Node
  3. Brands
  4. Flavors
  5. Faster brands and flavors
  6. Properties
  7. Introduction
  8. Crazy-level strictness
  9. Advanced use cases (pseudo dependent types)

@coderspirit/nominal

NPM version TypeScript License npm downloads Known Vulnerabilities Security Score

Nominal provides a powerful toolkit to apply nominal typing on Typescript with zero runtime overhead.

It offers three kinds of nominal types:

  • Brands: Brands basically match the traditional concept of nominal typing. Branded values can only belong to one brand, and branded variables only accept values with that same brand.
  • Flavors: Flavors are similar to brands, with one difference: flavored variables also accept unbranded/unflavored values with the same base type. They are very useful when dealing with "rigid" code generators or other cases where we would be forced to write tons of mappings just to content the type checker.
  • Properties: They are very useful to express things like logical and mathematical properties, but also to implement a weak form of dependent types.

While each type can only have either a brand or a flavor, we can easily combine brands or flavors with properties.

Install instructions

Node

# With NPM
npm install --save-dev @coderspirit/nominal

# Or with PNPM
pnpm add --save-dev @coderspirit/nominal

# Or with Yarn:
yarn add --dev @coderspirit/nominal

[!TIP] Note that if you are developing an application, it is fine to install @coderspirit/nominal as a development dependency, but it might be necessary to install it as a normal or peer dependency in case you are developing your own libraries based on it.

Brands

import { WithBrand } from '@coderspirit/nominal'

type Email = WithBrand<string, 'Email'>
type Username = WithBrand<string, 'Username'>

const email: Email = '[email protected]' as Email // Ok
const user: Username = 'admin' as Username // Ok
const text: string = email // OK
const anotherText: string = user // Ok

const eMail: Email = '[email protected]' // Error, as we don't have a cast here
const mail: Email = user // Error, as the brands don't match

Advice

  • Although we perform a "static cast" here, this should be done only when:
    • the value is a literal (as in the example)
    • in validation, sanitization and/or anticorruption layers.
  • One way to protect against other developers "forging" the type is to use symbols instead of strings as property keys or property values when defining the new nominal type.

Flavors

import { WithFlavor } from '@coderspirit/nominal'

type Email = WithFlavor<string, 'Email'>
type Username = WithFlavor<string, 'Username'>

const email: Email = '[email protected]' as Email // Ok
const user: Username = 'admin' as Username // Ok
const text: string = email // OK
const anotherText: string = user // Ok

const eMail: Email = '[email protected]' // Ok, flavors are more flexible than brands
const mail: Email = user // Error, as the flavors don't match

Advice

  • Although we perform a "static cast" here, this should be done only when:
    • the value is a literal (as in the example)
    • in validation, sanitization and/or anticorruption layers.
  • One way to protect against other developers "forging" the type is to use symbols instead of strings as property keys or property values when defining the new nominal type.

Faster brands and flavors

The types WithBrand and WithFlavor, although quite simple in their purpose, hide a quite complex machinery that exists for the sole purpose of maintaining full compatibility with other more complex types such as WithProperty.

Most times we won't really need to rely on such complex mechanisms because we apply WithBrandh and WithFlavor to basic types. So, if we want to minimize our compilation types, we can chose a simpler and faster implementation:

import {
  FastBrand,
  FastFlavor,
  WithBrand,
  WithFlavor
} from '@coderspirit/nominal'

// These two types are 100% equivalent, but the second one takes less time to be
// compiled. Notice that they are 100% equivalent only because they were applied
// to "basic" types (without other associated metadata, like `WithProperty`).
type SlowEmailType = WithBrand<string, 'Email'>
type FastEmailType = FastBrand<string, 'Email'>

// Same for flavors.
type SlowPhoneNumberType = WithFlavor<string, 'PhoneNumber'>
type FastPhoneNumberType = FastFlavor<string, 'PhoneNumber'>

Properties

Introduction

To define a new type with a property, we can do:

import { WithProperty } from '@coderspirit/nominal'
type Even = WithProperty<number, 'Parity', 'Even'>
const myEven: Even = 42 as Even

If we want to use the properties as simple tags, we can omit the property value, and it will implicitly default to true, although it's less flexible:

import { WithProperty } from '@coderspirit/nominal'
type Positive = WithProperty<number, 'Positive'>
const myPositive: Positive = 1 as Positive

Interesting properties

  • WithProperty is additive, commutative and idempotent.
  • The previous point means that we don't have to worry about the order of composition, we won't suffer typing inconsistencies because of that.

WithProperty can be combined in two ways, which are completely compatible:

  • "Classic" & type operator:
    type PositiveEven = WithProperty<number, 'Parity', 'Even'> & WithProperty<number, 'Positive'>
    
  • Nesting types:
    type PositiveEven = WithProperty<WithProperty<number, 'Positive'>, 'Parity', 'Even'>
    

Advice

  • Although we perform a "static cast" here, this should be done only when:
    • the value is a literal (as in the example)
    • in validation, sanitization and/or anticorruption layers.
  • One way to protect against other developers "forging" the type is to use symbols instead of strings as property keys or property values when defining the new nominal type.

Crazy-level strictness

If we want, we can even define "property types", to ensure that we don't set invalid values:

import { PropertyTypeDefinition, WithStrictProperty } from '@coderspirit/nominal'
type Parity = PropertyTypeDefinition<'Parity', 'Even' | 'Odd'>

// == WithProperty<number, 'Parity', 'Even'>
type Even = WithStrictProperty<number, Parity, 'Even'>

// == never
type Wrong = WithStrictProperty<number, Parity, 'Seven'>

Advanced use cases (pseudo dependent types)

Properties can be preserved across function boundaries

This feature can be very useful when we need to verify many properties for the same value and we don't want to lose this information along the way as the value is passed from one function to another.

function throwIfNotEven<T extends number>(v: T): WithProperty<T, 'Parity', 'Even'> {
  if (v % 2 == 1) throw new Error('Not Even!')
  return v as WithProperty<T, 'Even'>
}

function throwIfNotPositive<T extends number>(v: T): WithProperty<T, 'Sign', 'Positive'> {
  if (v <= 0) throw new Error('Not positive!')
  return v as WithProperty<T, 'Positive'>
}

const v1 = 42

// typeof v2 === WithProperty<number, 'Parity', 'Even'>
const v2 = throwIfNotEven(v1)

// typeof v3 extends WithProperty<number, 'Parity', 'Even'>
// typeof v3 extends WithProperty<number, 'Sign', 'Positive'>
const v3 = throwIfNotPositive(v2)

Chosing what properties to preserve across function boundaries

In the previous example, we could add many properties because we were just making assertions about the values. When we transform the passed values, we must be more careful about what we preserve.

As a simple example of what we are telling here, we can see that adding 1 to a numeric variable would flip its parity, so in that case we wouldn't want to keep that property on the return value.

type Even<N extends number = number> = WithProperty<N, 'Parity', 'Even'>
type Odd<N extends number = number> = WithProperty<N, 'Parity', 'Odd'>

// 1. 'Parity' is overwritten (when available)
// 2. 'Sign' is kept only if it's positive
// 3. We discard all other properties because they might stop being true
type PlusOneResult<N> = KeepProperties<
  N extends Even
    ? KeepPropertyIfValueMatches<Odd<N>, 'Sign', 'Positive'>
    : N extends Odd
    ? KeepPropertyIfValueMatches<Even<N>, 'Sign', 'Positive'>
    : KeepPropertyIfValueMatches<N, 'Sign', 'Positive'>,
  'Sign' | 'Parity'
>

function plusOne<N extends number>(v: N): PlusOneResult<N> {
  return v + 1 as PlusOneResult<N>
}

版本与发布记录

@coderspirit/nominal 共发布 23 个版本,首次发布于 2021/03/29;过去 12 个月发布了 0 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
4.1.1latest2024/08/122 年前
4.1.02024/08/092 年前
4.0.52024/08/092 年前
4.0.42024/08/092 年前
4.0.32024/04/092 年前
4.0.22024/04/082 年前

关于 @coderspirit/nominal 的常见问题

@coderspirit/nominal 是什么?

Powerful nominal types for your project

如何安装 @coderspirit/nominal?

在项目目录运行 npm install @coderspirit/nominal,或使用 pnpm add / yarn add / bun add。

@coderspirit/nominal 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@coderspirit/nominal 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@coderspirit/nominal 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@coderspirit/nominal 可以商用吗?

@coderspirit/nominal 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@coderspirit/nominal 的下载量有多少?

根据 npm 官方下载统计,@coderspirit/nominal 在 2026/10/10 – 2026/11/08 这 30 天内被下载 47,292 次,日均约 1,576 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@coderspirit/nominal 还在维护吗?

最新版本 4.1.1 发布于 2024/08/12(2 年前);至今共发布 23 个版本,其中过去 12 个月发布了 0 个;首次发布于 2021/03/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。