Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active August 28, 2025 22:45
Show Gist options
  • Select an option

  • Save christhekeele/acb739b1db0f30c52ba209f82f91b274 to your computer and use it in GitHub Desktop.

Select an option

Save christhekeele/acb739b1db0f30c52ba209f82f91b274 to your computer and use it in GitHub Desktop.
Nominal flavored and branded types for typescript.
// BRAND AND FLAVOR NOMINAL TYPING
//
// Typescript typing is structural: it only care about the
// data shape and primitive types. For example, a type alias
// is interchangable with its underlying primitive type:
// type FooCode = string;
//
// Multiple type aliases are interchangable with each other:
// type FooCode = string;
// type BarCode = string;
// const foo: FooCode = "fizzbuzz";
// const bar: BarCode = foo;
//
// Nominal typing lets us tell typescript,
// "Because we have given this type a name,
// it is incompatable with similarly-structured types
// of a different name".
//
// For more, see references:
// https://spin.atomicobject.com/typescript-flexible-nominal-typing/
// https://prosopo.io/blog/typescript-branding/
const NominalType = Symbol('NominalType');
// FLAVORING
//
// A nominal "flavored" type can accept the underlying type, but
// cannot be used with other flavors elsewhere:
//
// type FooCode = Flavor<string, "Foo">;
// type BarCode = Flavor<string, "Bar">;
// const foo: FooCode = "fizzbuzz";
// const bar: BarCode = foo;
// !> Type 'FooCode' is not assignable to type 'BarCode'.
//
//
// This allows us to begin using the type system to declare
// exactly what kind of string we want to be working with,
// and complain if we accidentally mix string types in datastructures.
// However, Flavor types can be assigned generic types,
// like foo = "fizzbuzz" above.
export type Flavor<Type, Name> = Type & {
[NominalType]?: Name;
};
// BRANDING
//
// A nominal "branded" type behaves the same, but will not accept
// the underlying underlying primiative type without explicit co-ercion:
//
// type FooCode = Flavor<string, "Foo">;
// const foo: FooCode = "fizzbuzz";
// !> Type 'string' is not assignable to type 'FooCode'.
//
// This forces us to explicitly declare when we are allowing a primitive type
// to become a stronger branded type:
//
// type FooCode = Flavor<string, "Foo">;
// const foo: FooCode = "fizzbuzz" as FooCode;
export type Brand<Type, Name> = Type & {
[NominalType]: Name;
};
// POLYMORPHISM
//
// Nominal types can share names, but will still not be
// assignable to each other:
//
// type FooCode = Flavor<string, "Foo">;
// type FooId = Flavor<number, "Foo">;
//
// const fooCode: FooCode = "fizz";
// const fooId: FooId = fooCode;
// !> Type 'FooCode' is not assignable to type 'FooId'.
// !> Type 'FooCode' is not assignable to type 'number'.
//
// You can construct explicit unions to work around this
// if you want polymorphism:
//
// type FooCode = Flavor<string, "Foo">;
// type FooId = Flavor<number, "Foo">;
// type Fooish = FooCode | FooId;
//
// const fooCode: FooCode = "fizz";
// const fooId: FooId = 1;
// let fooish: Fooish;
// fooish = fooCode;
// fooish = fooId;
//
// Alternatively, you can construct a nominal type polymorphic
// on any type with the same name, provided you are using Brands:
//
// type FooCode = Brand<string, "Foo">;
// type FooId = Brand<number, "Foo">;
// type Fooish = Named<"Foo">;
//
// const fooCode: FooCode = "fizz" as FooCode;
// const fooId: FooId = 1 as FooId;
// let fooish: Fooish;
// fooish = fooCode;
// fooish = fooId;
//
// If using Flavors instead of Brands, you will get the error:
//
// type FooCode = Flavor<string, "Foo">;
// type FooId = Flavor<number, "Foo">;
// type Fooish = Named<"Foo">;
//
// const fooCode: FooCode = "fizz" as FooCode;
// const fooId: FooId = 1 as FooId;
// let fooish: Fooish;
// fooish = fooCode;
// !> Type 'FooCode' is not assignable to type 'Fooish'.
// !> Types of property '[NominalType]' are incompatible.
// !> Type '"Foo" | undefined' is not assignable to type '"Foo"'.
// !> Type 'undefined' is not assignable to type '"Foo"'.
export type Named<Name> = Record<typeof NominalType, Name>;
// ERASURE
//
// As Flavors and Brands are assignable to their primitives, you usually
// don't need to worry much about erasing the norminal type.
// However, should you want to, use the Primitive helper.
// type FooCode = Brand<string, "Foo">;
// const fooCode: FooCode = "fizz" as FooCode;
// // This works fine:
// // const string: string = fooCode;
// // But for explicitness, you can also do:
// const string = fooCode as Primitive<string>;
export type Primitive<Type> = Exclude<Type, typeof NominalType>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment