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
Therefore I created several files
My problem is that my tests seem to keep failing when trying to compare the strings.
I am currently trying to trace down some unexpected behaviour with branded types.
To explain: I have several URL types
EntityTypeUrlEntityTypeUrl, DataTypeUrlDataTypeUrl, PropertyTypeUrlPropertyTypeUrl, VersionedUrlVersionedUrl, BaseUrlBaseUrlEntityTypeUrlEntityTypeUrl, DataTypeUrlDataTypeUrl and PropertyTypeUrlPropertyTypeUrl are all VersionedUrlVersionedUrl, so any function working with a VersionedUrlVersionedUrl should work with either, and a VersionedUrlVersionedUrl the following template string ${T}/v/${number}${T}/v/${number}Therefore I created several files
BaseUrl.tsBaseUrl.tsimport * 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;
}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.tsVersionedUrl.tsimport * 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);
}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.tsPropertyTypeUrl.tsimport * 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;
}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.
