feat(schema): add Schema.opaqueBrand for fully opaque primitive types#1570
feat(schema): add Schema.opaqueBrand for fully opaque primitive types#1570kitlangton wants to merge 2 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: df07d9a The changes in this PR will be included in the next version bump. This PR includes changesets to release 26 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Bundle Size Analysis
|
📊 JSDoc Documentation Analysis📈 Current Analysis ResultsThis comment is automatically updated on each push. View the analysis script for details. |
There was a problem hiding this comment.
Pull request overview
Adds a new Schema.opaqueBrand branding combinator to create fully opaque branded primitive types, enabling stricter type boundaries during migrations (e.g., opaque IDs not assignable to string).
Changes:
- Implement
Schema.opaqueBrandwith the same runtime AST branding asSchema.brand, but with opaque output typing (Brand.Brand<B>). - Add runtime test coverage for AST brand annotations when using
opaqueBrand. - Add dtslint coverage for opaque output typing, rebranding behavior, and non-assignability to “stringly” APIs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/effect/src/Schema.ts | Introduces opaqueBrand interface + Schema.opaqueBrand combinator with opaque output typing. |
| packages/effect/test/schema/Schema.test.ts | Adds runtime assertion that opaqueBrand contributes the expected brands annotation in the AST. |
| packages/effect/dtslint/schema/Schema.tst.ts | Adds type-level tests for opaque output, rebranding semantics, and assignability boundaries. |
| .changeset/tame-wombats-juggle.md | Declares a minor release changeset describing the new API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/effect/src/Schema.ts
Outdated
| * @since 4.0.0 | ||
| */ | ||
| export function opaqueBrand<B extends string>(identifier: B) { | ||
| return <S extends Top>(schema: S): opaqueBrand<S["~rebuild.out"], B> => |
There was a problem hiding this comment.
opaqueBrand currently accepts any Top schema, but it erases the schema’s original Type and replaces it with Brand.Brand<B>. This is type-unsound for non-primitive schemas (e.g. Schema.Struct(...).pipe(Schema.opaqueBrand("X")) would validate to an object at runtime but be typed as Brand.Brand<"X">). Consider constraining S so opaqueBrand is only callable when S["Type"] is a primitive (or otherwise non-object) type, or make the return type conditional on S["Type"] being primitive.
| return <S extends Top>(schema: S): opaqueBrand<S["~rebuild.out"], B> => | |
| return <S extends Top>(schema: S): S["Type"] extends object ? never : opaqueBrand<S["~rebuild.out"], B> => |
packages/effect/src/Schema.ts
Outdated
| return <S extends Top>(schema: S): opaqueBrand<S["~rebuild.out"], B> => | ||
| make(AST.brand(schema.ast, identifier), { schema, identifier }) | ||
| } |
There was a problem hiding this comment.
Because opaqueBrand uses AST.brand(...) exactly like brand, downstream consumers that derive TypeScript types from AST brand annotations (e.g. SchemaRepresentation.toCodeDocument which renders brands as T & Brand.Brand<...>) cannot distinguish opaque vs transparent brands. If SchemaRepresentation/code generation is expected to reflect the new opaque typing, consider adding an additional AST annotation/flag for opaque brands (while still keeping annotations.brands for runtime parity) and updating the representation logic to honor it.
|
If I understand correctly, this is a request for a newtype implementation, right? |
Also it should probably carry the original type as a reference, like the old newtype implementation did |
Replace Schema.opaqueBrand with Schema.newtype, using a NewtypeBrand<K, From> phantom type that carries the original type for recovery via NewtypeFrom<T>. Uses a separate "newtypes" annotation independent of "brands".
cd8b743 to
df07d9a
Compare
NOTICE: this PR description was written by robots under the supervision of Kit Langton
Type
Description
This PR adds
Schema.opaqueBrandfor fully opaque primitive types.Schema.brand(same validation / AST branding).Brand.Brand<B>instead ofT & Brand.Brand<B>.Why
In incremental migrations, we want strict boundaries between new typed IDs and old stringly APIs.
Example:
WorkspaceIdshould not be passable to legacy functions likedirectoryId: string.Schema.opaqueBrandenforces that boundary.Example
Tests
Schema.opaqueBrandbranding metadata.