Effect CommunityEC
Effect Communityโ€ข2y agoโ€ข
83 replies
Bilal

Branded strings in vitest

I come again with questions about branded types, sorry ๐Ÿ˜… , this time hopefully with a bit more information.

I am currently trying to trace down some unexpected behaviour with branded types.

To explain: I have several URL types EntityTypeUrl, DataTypeUrl, PropertyTypeUrl, VersionedUrl, BaseUrl

EntityTypeUrl, DataTypeUrl and PropertyTypeUrl are all VersionedUrl, so any function working with a VersionedUrl should work with either, and a VersionedUrl the following template string ${T}/v/${number}

Therefore I created several files

BaseUrl.ts
import * as S from "@effect/schema/Schema";
import { Brand } from "effect";

import * as Url from "./internal/Url";

const TypeId: unique symbol = Symbol.for("@blockprotocol/graph/BaseUrl");
export type TypeId = typeof TypeId;

export type BaseUrl<T extends string = string> = T & Brand.Brand<TypeId>;
const BaseUrlBrand = Brand.nominal<BaseUrl>();

export const BaseUrl: S.Schema<BaseUrl, string> = Url.Url.pipe(
  S.fromBrand(BaseUrlBrand),
);

export function parseOrThrow<T extends string>(
  value: T,
): T & Brand.Brand<TypeId> {
  return S.decodeSync(BaseUrl)(value) as never;
}


VersionedUrl.ts
import * as S from "@effect/schema/Schema";
import { Brand } from "effect";

import * as BaseUrl from "./BaseUrl";
import * as Url from "./internal/Url";

const TypeId: unique symbol = Symbol.for("@blockprotocol/graph/VersionedUrl");
export type TypeId = typeof TypeId;

export const Pattern = /^(.+\/)v\/(\d+)$/;
export type Pattern<T extends string> = `${T}/v/${number}`;

export type VersionedUrl<T extends BaseUrl.BaseUrl = BaseUrl.BaseUrl> =
  Pattern<T> & Brand.Brand<TypeId>;
const VersionedUrlBrand = Brand.nominal<VersionedUrl>();

// Pattern ensures that we obey the versioning scheme, therefore using the literal pattern
// cast is okay here.
export const VersionedUrl: S.Schema<VersionedUrl, string> = Url.Url.pipe(
  S.pattern(Pattern),
  (schema) => schema as unknown as S.Schema<Pattern<BaseUrl.BaseUrl>, string>,
  S.fromBrand(VersionedUrlBrand),
);

export function parseOrThrow<T extends string>(
  value: T,
): T extends Pattern<infer U>
  ? VersionedUrl<U & Brand.Brand<BaseUrl.TypeId>>
  : VersionedUrl {
  return S.decodeSync(VersionedUrl)(value) as never;
}

type UnbrandedBase<T> = T extends Pattern<infer U> ? U : never;
export type Base<T> = UnbrandedBase<Brand.Brand.Unbranded<T>>;
export function base<T extends VersionedUrl>(value: T): Base<T> {
  // the value is never null or undefined, because `Schema` guarantees a well-formed value.
  const match = value.match(Pattern)!;

  // again, value is guaranteed to be a string, because `Schema` guarantees a well-formed value.
  return match[1] as never;
}

export function version(value: VersionedUrl): number {
  // the value is never null or undefined, because `Schema` guarantees a well-formed value.
  const match = value.match(Pattern)!;

  // again, value is guaranteed to be a number, because `Schema` guarantees a well-formed value.
  return parseInt(match[2]!, 10);
}


PropertyTypeUrl.ts
import * as S from "@effect/schema/Schema";
import { Brand } from "effect";

import * as BaseUrl from "../BaseUrl";
import * as VersionedUrl from "../VersionedUrl";
import { Pattern } from "../VersionedUrl";

const TypeId: unique symbol = Symbol.for(
  "@blockprotocol/graph/ontology/PropertyTypeUrl",
);
export type TypeId = typeof TypeId;

export const PropertyTypeUrl: S.Schema<PropertyTypeUrl, string> =
  VersionedUrl.VersionedUrl.pipe(S.brand(TypeId));

export type PropertyTypeUrl<T extends BaseUrl.BaseUrl = BaseUrl.BaseUrl> =
  VersionedUrl.VersionedUrl<T> & Brand.Brand<TypeId>;

export function parseOrThrow<T extends string>(
  value: T,
): T extends VersionedUrl.Pattern<infer U>
  ? PropertyTypeUrl<U & Brand.Brand<BaseUrl.TypeId>>
  : PropertyTypeUrl {
  return S.decodeSync(PropertyTypeUrl)(value) as never;
}


My problem is that my tests seem to keep failing when trying to compare the strings.
Was this page helpful?