Skip to content

feat(schema): add Schema.opaqueBrand for fully opaque primitive types#1570

Open
kitlangton wants to merge 2 commits intomainfrom
feat/schema-opaque-brand
Open

feat(schema): add Schema.opaqueBrand for fully opaque primitive types#1570
kitlangton wants to merge 2 commits intomainfrom
feat/schema-opaque-brand

Conversation

@kitlangton
Copy link
Contributor

NOTICE: this PR description was written by robots under the supervision of Kit Langton

Type

  • Feature

Description

This PR adds Schema.opaqueBrand for fully opaque primitive types.

  • Runtime behavior matches Schema.brand (same validation / AST branding).
  • Type output is opaque: Brand.Brand<B> instead of T & Brand.Brand<B>.
  • Type-level behavior is rebranding: previous brand composition is replaced by the new opaque brand.

Why

In incremental migrations, we want strict boundaries between new typed IDs and old stringly APIs.

Example: WorkspaceId should not be passable to legacy functions like directoryId: string.
Schema.opaqueBrand enforces that boundary.

Example

type WorkspaceIdTransparent = string & Brand.Brand<"WorkspaceId">
type WorkspaceIdOpaque = Brand.Brand<"WorkspaceId">

declare const legacyDirectoryApi: (directoryId: string) => void
declare const workspaceIdOpaque: WorkspaceIdOpaque

legacyDirectoryApi(workspaceIdOpaque) // type error

Tests

  • Added runtime coverage for Schema.opaqueBrand branding metadata.
  • Added dtslint coverage for opaque output type, rebranding behavior, and non-assignability to stringly APIs.

@changeset-bot
Copy link

changeset-bot bot commented Mar 1, 2026

🦋 Changeset detected

Latest commit: df07d9a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 26 packages
Name Type
effect Major
@effect/opentelemetry Major
@effect/platform-browser Major
@effect/platform-bun Major
@effect/platform-node-shared Major
@effect/platform-node Major
@effect/vitest Major
@effect/ai-anthropic Major
@effect/ai-openai-compat Major
@effect/ai-openai Major
@effect/ai-openrouter Major
@effect/atom-react Major
@effect/atom-solid Major
@effect/atom-vue Major
@effect/sql-clickhouse Major
@effect/sql-d1 Major
@effect/sql-libsql Major
@effect/sql-mssql Major
@effect/sql-mysql2 Major
@effect/sql-pg Major
@effect/sql-sqlite-bun Major
@effect/sql-sqlite-do Major
@effect/sql-sqlite-node Major
@effect/sql-sqlite-react-native Major
@effect/sql-sqlite-wasm Major
@effect/openapi-generator Major

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

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

Bundle Size Analysis

File Name Current Size Previous Size Difference
basic.ts 6.37 KB 6.37 KB 0.00 KB (0.00%)
batching.ts 8.53 KB 8.53 KB 0.00 KB (0.00%)
brand.ts 6.29 KB 6.29 KB 0.00 KB (0.00%)
cache.ts 9.69 KB 9.69 KB 0.00 KB (0.00%)
config.ts 16.73 KB 16.73 KB 0.00 KB (0.00%)
differ.ts 15.09 KB 15.09 KB 0.00 KB (0.00%)
http-client.ts 19.13 KB 19.13 KB 0.00 KB (0.00%)
logger.ts 8.86 KB 8.86 KB 0.00 KB (0.00%)
metric.ts 8.22 KB 8.22 KB 0.00 KB (0.00%)
optic.ts 7.54 KB 7.54 KB 0.00 KB (0.00%)
pubsub.ts 13.36 KB 13.36 KB 0.00 KB (0.00%)
queue.ts 10.57 KB 10.57 KB 0.00 KB (0.00%)
schedule.ts 9.95 KB 9.95 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 24.39 KB 24.39 KB 0.00 KB (0.00%)
schema-string-transformation.ts 11.65 KB 11.65 KB 0.00 KB (0.00%)
schema-string.ts 9.94 KB 9.94 KB 0.00 KB (0.00%)
schema-template-literal.ts 12.56 KB 12.56 KB 0.00 KB (0.00%)
schema-toArbitraryLazy.ts 16.79 KB 16.79 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 19.55 KB 19.47 KB +0.08 KB (+0.39%)
schema-toCodecJson.ts 15.96 KB 15.96 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 15.94 KB 15.94 KB 0.00 KB (0.00%)
schema-toFormatter.ts 15.79 KB 15.79 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 18.23 KB 18.23 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 16.35 KB 16.35 KB 0.00 KB (0.00%)
schema.ts 15.42 KB 15.42 KB 0.00 KB (0.00%)
stm.ts 11.49 KB 11.49 KB 0.00 KB (0.00%)
stream.ts 8.77 KB 8.77 KB 0.00 KB (0.00%)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

📊 JSDoc Documentation Analysis

📈 Current Analysis Results
Analyzing 134 TypeScript files in packages/effect/src/ (including schema and config subdirectories)...

============================================================
         EFFECT JSDOC ANALYSIS REPORT
============================================================

📊 SUMMARY STATISTICS
------------------------------
Total files analyzed: 134
Total exported members: 4226
Missing @example: 2080 (49.2%)
Missing @category: 459 (10.9%)

🎯 TOP FILES NEEDING ATTENTION
----------------------------------------
1. Schema.ts
   📝 488 missing examples, 🏷️  273 missing categories
   📦 488 total exports
2. Array.ts
   📝 135 missing examples, 🏷️  2 missing categories
   📦 140 total exports
3. SchemaRepresentation.ts
   📝 96 missing examples, 🏷️  12 missing categories
   📦 96 total exports
4. SchemaAST.ts
   📝 77 missing examples, 🏷️  21 missing categories
   📦 77 total exports
5. Channel.ts
   📝 79 missing examples, 🏷️  0 missing categories
   📦 150 total exports
6. Cause.ts
   📝 75 missing examples, 🏷️  2 missing categories
   📦 75 total exports
7. Option.ts
   📝 66 missing examples, 🏷️  3 missing categories
   📦 66 total exports
8. Sink.ts
   📝 64 missing examples, 🏷️  2 missing categories
   📦 81 total exports
9. SchemaTransformation.ts
   📝 37 missing examples, 🏷️  26 missing categories
   📦 37 total exports
10. Predicate.ts
   📝 57 missing examples, 🏷️  0 missing categories
   📦 57 total exports
11. SchemaGetter.ts
   📝 49 missing examples, 🏷️  0 missing categories
   📦 49 total exports
12. Result.ts
   📝 46 missing examples, 🏷️  0 missing categories
   📦 46 total exports
13. Config.ts
   📝 34 missing examples, 🏷️  5 missing categories
   📦 34 total exports
14. Types.ts
   📝 38 missing examples, 🏷️  0 missing categories
   📦 38 total exports
15. Effect.ts
   📝 31 missing examples, 🏷️  2 missing categories
   📦 256 total exports

✅ PERFECTLY DOCUMENTED FILES
-----------------------------------
   Chunk.ts (86 exports)
   Clock.ts (5 exports)
   FiberHandle.ts (15 exports)
   FiberMap.ts (19 exports)
   FiberSet.ts (14 exports)
   HKT.ts (4 exports)
   HashMap.ts (44 exports)
   HashSet.ts (21 exports)
   Latch.ts (3 exports)
   Match.ts (57 exports)
   MutableHashSet.ts (9 exports)
   MutableRef.ts (17 exports)
   NonEmptyIterable.ts (3 exports)
   Random.ts (9 exports)
   Redacted.ts (9 exports)
   RegExp.ts (3 exports)
   Symbol.ts (1 exports)
   Trie.ts (29 exports)
   TxChunk.ts (22 exports)
   TxDeferred.ts (7 exports)
   TxHashMap.ts (41 exports)
   TxHashSet.ts (24 exports)
   TxPriorityQueue.ts (19 exports)
   TxReentrantLock.ts (18 exports)
   TxRef.ts (7 exports)
   TxSemaphore.ts (14 exports)
   TxSubscriptionRef.ts (12 exports)
   Unify.ts (8 exports)
   index.ts (0 exports)

🔍 SAMPLE MISSING ITEMS FROM Schema.ts
-----------------------------------
   Optionality (type, line 64): missing example, category
   Mutability (type, line 71): missing example, category
   ConstructorDefault (type, line 78): missing example, category
   MakeOptions (interface, line 86): missing example, category
   Bottom (interface, line 110): missing example, category
   declareConstructor (interface, line 163): missing example, category
   declareConstructor (function, line 187): missing example
   declare (interface, line 211): missing example
   declare (function, line 220): missing example, category
   revealBottom (function, line 240): missing example, category

📋 BREAKDOWN BY EXPORT TYPE
-----------------------------------
const: 1065 missing examples, 131 missing categories
interface: 321 missing examples, 134 missing categories
type: 241 missing examples, 76 missing categories
function: 346 missing examples, 97 missing categories
namespace: 48 missing examples, 20 missing categories
class: 59 missing examples, 1 missing categories

📈 DOCUMENTATION PROGRESS
------------------------------
Examples: 2146/4226 (50.8% complete)
Categories: 3767/4226 (89.1% complete)

============================================================
Analysis complete! 2539 items need attention.
============================================================

📄 Detailed results saved to: jsdoc-analysis-results.json

This comment is automatically updated on each push. View the analysis script for details.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.opaqueBrand with the same runtime AST branding as Schema.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.

* @since 4.0.0
*/
export function opaqueBrand<B extends string>(identifier: B) {
return <S extends Top>(schema: S): opaqueBrand<S["~rebuild.out"], B> =>
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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> =>

Copilot uses AI. Check for mistakes.
Comment on lines +2720 to +2722
return <S extends Top>(schema: S): opaqueBrand<S["~rebuild.out"], B> =>
make(AST.brand(schema.ast, identifier), { schema, identifier })
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@gcanti
Copy link
Contributor

gcanti commented Mar 3, 2026

If I understand correctly, this is a request for a newtype implementation, right?
If so, I agree with the Copilot observation. It should use a different runtime implementation, similar to Schema.brand, but with a different annotation to accumulate identifiers. I would also explicitly name it "newtype" since that is what it is.

@mikearnaldi
Copy link
Member

If I understand correctly, this is a request for a newtype implementation, right? If so, I agree with the Copilot observation. It should use a different runtime implementation, similar to Schema.brand, but with a different annotation to accumulate identifiers. I would also explicitly name it "newtype" since that is what it is.

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".
@kitlangton kitlangton force-pushed the feat/schema-opaque-brand branch from cd8b743 to df07d9a Compare March 5, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants