Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5ccf24e
Bump version to 6.0.0-beta and LKG
typescript-bot Feb 9, 2026
8efd7e8
Merge remote-tracking branch 'origin/main' into release-6.0
typescript-bot Mar 3, 2026
af4caac
Update LKG
typescript-bot Mar 3, 2026
e175b69
Bump version to 6.0.1-rc and LKG
typescript-bot Mar 3, 2026
35ff23d
🤖 Pick PR #63163 (Port anyFunctionType subtype fix an...) into releas…
typescript-bot Mar 3, 2026
9e72ab7
🤖 Pick PR #63239 (Fix missing lib files in reused pro...) into releas…
typescript-bot Mar 13, 2026
607a22a
Bump version to 6.0.2 and LKG
typescript-bot Mar 16, 2026
0725fb4
🤖 Pick PR #63310 (Mark class property initializers as...) into releas…
typescript-bot Apr 6, 2026
ad1c695
🤖 Pick PR #63368 (Harden ATA package name filtering) into release-6.0…
typescript-bot Apr 7, 2026
eeae9dd
🤖 Pick PR #63401 (Also check package name validity in...) into releas…
typescript-bot Apr 15, 2026
050880c
Bump version to 6.0.3 and LKG
typescript-bot Apr 15, 2026
d5e2be8
Emit static Effect schema declarations
patroza Jun 21, 2026
49fe261
feat(declarations): facade S.Class/TaggedClass/ErrorClass/TaggedError…
patroza Jun 21, 2026
789e108
feat(declarations): facade top-level S.Struct/TaggedStruct schema consts
patroza Jun 21, 2026
40ad88e
fix(declarations): carry S.Class 'identifier' static on the facade base
patroza Jun 22, 2026
043f5f7
revert: drop per-model 'identifier' emit — it lives on the facade int…
patroza Jun 22, 2026
54b87c4
feat: emit S.StructFacade from effect-app (drop scanner-local import-…
patroza Jun 22, 2026
0f1017f
docs: link the effect-app fork PRs
patroza Jun 22, 2026
98a5389
docs: add before/after .d.ts samples (Opaque/Opaque+Encoded/Class/Cla…
patroza Jun 22, 2026
01ae23c
docs: the win compounds per-program (no cross-program instantiation c…
patroza Jun 22, 2026
bb27443
docs: correctness fix too — facade removes silent unknown/any degrada…
patroza Jun 22, 2026
cb7b78d
fix: normalize generated facade imports
patroza Jun 22, 2026
d26f689
fix: materialize legacy schema encoded placeholders
patroza Jun 22, 2026
8b75d6e
add fields
patroza Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions docs/effect-schema-facade-emit-samples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Facade emit — before/after samples

Real `.d.ts` output for each schema-model kind, **stock tsc 6.0.3** vs the **patched** compiler
(effect-app ≥ 4.0.0-beta.280). Generated from the fixture below; long `withConstructorDefault` /
`mapFields` / `copy` machinery is elided as `…` for readability — the point is the **base type**
and the **generated `namespace` / `interface`**.

## Source (fixture)

```ts
import * as S from "effect-app/Schema"

// 1. Opaque<X> — no Encoded namespace
export class OpaqueNoEncoded extends S.Opaque<OpaqueNoEncoded>()(S.Struct({
name: S.String,
age: S.Number
})) {}

// 2. Opaque<X, X.Encoded>
export class OpaqueWithEncoded extends S.Opaque<OpaqueWithEncoded, OpaqueWithEncoded.Encoded>()(S.Struct({
id: S.String,
count: S.Number
})) {}
export namespace OpaqueWithEncoded {
export interface Encoded extends S.StructNestedEncoded<typeof OpaqueWithEncoded> {}
}

// 3. Class<X>
export class ClassNoEncoded extends S.Class<ClassNoEncoded>("ClassNoEncoded")({ title: S.String }) {}

// 4. Class<X, X.Encoded>
export class ClassWithEncoded extends S.Class<ClassWithEncoded, ClassWithEncoded.Encoded>("ClassWithEncoded")({ label: S.String }) {}
export namespace ClassWithEncoded {
export interface Encoded extends S.StructNestedEncoded<typeof ClassWithEncoded> {}
}

// 5. Struct
export const MyStruct = S.Struct({ a: S.String, b: S.Number })
export type MyStruct = typeof MyStruct.Type
```

---

## 1. `Opaque<X>` (no Encoded)

**Stock** — the base carries the full inline `S.Struct<{…}>`; no namespace at all:

```ts
declare const OpaqueNoEncoded_base: S.Opaque<OpaqueNoEncoded, typeof S.ExtendedSchemaNoEncoded, S.Struct<{
readonly name: S.String;
readonly age: import("effect/Schema").Number & { withConstructorDefault: …; withDecodingDefaultType: … };
}>, {}> & Omit<S.Struct<{
readonly name: S.String;
readonly age: import("effect/Schema").Number & { … };
}>, keyof S.Top>;
export declare class OpaqueNoEncoded extends OpaqueNoEncoded_base {}
```

**Patched** — the `S.Struct<{…}>` is gone; base is the compact facade, and `Encoded`/`Make`/services
are synthesized into a namespace (none existed in source):

```ts
declare const OpaqueNoEncoded_base: S.OpaqueFacade<OpaqueNoEncoded, OpaqueNoEncoded.Encoded, OpaqueNoEncoded.Make, OpaqueNoEncoded.DecodingServices, OpaqueNoEncoded.EncodingServices, {}> & {
readonly fields: { readonly name: S.String; readonly age: … }; // statics preserved, elided
mapFields: …; readonly copy: …;
};
export declare class OpaqueNoEncoded extends OpaqueNoEncoded_base {}
export interface OpaqueNoEncoded { readonly name: string; readonly age: number; }
export declare namespace OpaqueNoEncoded {
interface Encoded { readonly name: string; readonly age: number; }
interface Make { readonly name: string; readonly age: number; }
type DecodingServices = never;
type EncodingServices = never;
}
```

## 2. `Opaque<X, X.Encoded>`

**Stock** — same inline `S.Struct<{…}>` in the base; only the source-written `Encoded extends StructNestedEncoded` namespace:

```ts
declare const OpaqueWithEncoded_base: S.Opaque<OpaqueWithEncoded, OpaqueWithEncoded.Encoded, S.Struct<{
readonly id: S.String;
readonly count: import("effect/Schema").Number & { … };
}>, {}> & Omit<S.Struct<{ … }>, keyof S.Top>;
export declare class OpaqueWithEncoded extends OpaqueWithEncoded_base {}
export declare namespace OpaqueWithEncoded {
interface Encoded extends S.StructNestedEncoded<typeof OpaqueWithEncoded> {} // conditional, recomputed per consumer
}
```

**Patched** — identical shape to case 1: the supplied `Encoded` is materialized to a literal, and `Make`/services are added:

```ts
declare const OpaqueWithEncoded_base: S.OpaqueFacade<OpaqueWithEncoded, OpaqueWithEncoded.Encoded, OpaqueWithEncoded.Make, OpaqueWithEncoded.DecodingServices, OpaqueWithEncoded.EncodingServices, {}> & { … statics … };
export declare class OpaqueWithEncoded extends OpaqueWithEncoded_base {}
export interface OpaqueWithEncoded { readonly id: string; readonly count: number; }
export declare namespace OpaqueWithEncoded {
interface Encoded { readonly id: string; readonly count: number; } // literal, not conditional
interface Make { readonly id: string; readonly count: number; }
type DecodingServices = never;
type EncodingServices = never;
}
```

> `Opaque<X>` and `Opaque<X, X.Encoded>` produce the **same** patched output — the facade synthesizes
> the full namespace either way.

## 3. `Class<X>`

**Stock** — `EnhancedClass` carries the full inline `Struct<{…}>`; no namespace:

```ts
declare const ClassNoEncoded_base: S.EnhancedClass<ClassNoEncoded, import("effect/Schema").Struct<{
title: S.String;
}>, {}>;
export declare class ClassNoEncoded extends ClassNoEncoded_base {}
```

**Patched** — `S.OpaqueClassFacade` (+ `identifier`/`fields`/… from the effect-app facade & statics), full namespace:

```ts
declare const ClassNoEncoded_base: S.OpaqueClassFacade<ClassNoEncoded, ClassNoEncoded.Encoded, ClassNoEncoded.Make, ClassNoEncoded.DecodingServices, ClassNoEncoded.EncodingServices, {}> & { readonly fields: { title: S.String }; mapFields: …; readonly copy: …; };
export declare class ClassNoEncoded extends ClassNoEncoded_base {}
export interface ClassNoEncoded { readonly title: string; }
export declare namespace ClassNoEncoded {
interface Encoded { readonly title: string; }
interface Make { readonly title: string; }
type DecodingServices = never;
type EncodingServices = never;
}
```

## 4. `Class<X, X.Encoded>`

**Stock** — `EnhancedClass` with the inline `Struct<{…}>` wrapped to override `Encoded`:

```ts
declare const ClassWithEncoded_base: S.EnhancedClass<ClassWithEncoded, Omit<import("effect/Schema").Struct<{
label: S.String;
}>, "Encoded"> & { readonly Encoded: ClassWithEncoded.Encoded; }, {}>;
export declare class ClassWithEncoded extends ClassWithEncoded_base {}
export declare namespace ClassWithEncoded {
interface Encoded extends S.StructNestedEncoded<typeof ClassWithEncoded> {}
}
```

**Patched** — same as case 3 (`OpaqueClassFacade` + materialized namespace):

```ts
declare const ClassWithEncoded_base: S.OpaqueClassFacade<ClassWithEncoded, ClassWithEncoded.Encoded, ClassWithEncoded.Make, ClassWithEncoded.DecodingServices, ClassWithEncoded.EncodingServices, {}> & { … statics … };
export declare class ClassWithEncoded extends ClassWithEncoded_base {}
export interface ClassWithEncoded { readonly label: string; }
export declare namespace ClassWithEncoded {
interface Encoded { readonly label: string; }
interface Make { readonly label: string; }
type DecodingServices = never;
type EncodingServices = never;
}
```

> Error models (`S.ErrorClass`/`S.TaggedErrorClass`) are identical but emit `S.OpaqueErrorFacadeClass`
> with the `Cause.YieldableError` brand preserved.

## 5. `Struct`

**Stock** — the giant inline `S.Struct<{…}>` is the const's type; only a `type X = typeof X.Type` companion:

```ts
export declare const MyStruct: S.Struct<{
readonly a: S.String;
readonly b: import("effect/Schema").Number & { withConstructorDefault: …; withDecodingDefaultType: … };
}>;
export type MyStruct = typeof MyStruct.Type;
```

**Patched** — `S.StructFacade` (a real effect-app `Struct<Fields>`, Workflow-compatible), with `Fields`/`Encoded`/`Make`/services materialized; the `type X` companion is dropped for an `interface X`:

```ts
export declare const MyStruct: S.StructFacade<MyStruct, MyStruct.Encoded, MyStruct.Make, MyStruct.DecodingServices, MyStruct.EncodingServices, MyStruct.Fields>;
export interface MyStruct { readonly a: string; readonly b: number; }
export declare namespace MyStruct {
interface Fields { readonly a: S.String; readonly b: … }
interface Encoded { readonly a: string; readonly b: number; }
interface Make { readonly a: string; readonly b: number; }
type DecodingServices = never;
type EncodingServices = never;
}
```

---

The win: stock inlines the full `S.Struct<{…field machinery…}>` in every model's emitted base (and
re-derives `Encoded`/`Type` at each consumer); patched replaces it with a compact facade + named
namespace interfaces materialized **once**, so consumers read `X.Encoded` / `X.Type` by name.
See `effect-schema-facade-emit.md` for the why/how and measured −29.5% instantiation drop.
183 changes: 183 additions & 0 deletions docs/effect-schema-facade-emit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Effect schema facade emit (`.d.ts` declaration step)

Fork of `microsoft/TypeScript` that expands effect-app schema models into compact, named
**facades** when it writes declaration files — moving what was an in-source "native model
codegen" step into the compiler's `.d.ts` emitter. Source files stay clean; every project
reference consumes a `.d.ts` where each model is expanded **once**, so downstream consumers read
cheap named interfaces instead of re-instantiating schema generics.

Mirrored 1:1 in the Go compiler: **effect-app/typescript-go** (and as `_patches/` on
**effect-app/tsgo**).

---

## What

For every effect-app schema model the declaration emitter rewrites the emitted type and generates
a companion namespace:

- **Class / error models** (`S.Class`, `S.TaggedClass`, `S.ErrorClass`, `S.TaggedErrorClass`):
the hoisted `X_base` const's type is rewritten from `S.EnhancedClass<X, S.Struct<{…full…}>, Inherited>`
to the matching facade, and a `namespace X { Encoded; Make; DecodingServices; EncodingServices }`
plus a `Type` `interface X` are generated (when absent).
- **Opaque models / requests** (`S.Opaque<X>`, `Req.Query`/`Req.Command`): same, via `OpaqueFacade`.
- **Struct models** (`S.Struct`, `S.TaggedStruct` — top-level `const`): the giant inline
`S.Struct<{…}>` annotation on the const is replaced by `S.StructFacade<…>`, with a sibling
`interface X` (decoded `Self`) and a **type-only** `declare namespace X` (`Fields`, `Encoded`,
`Make`, services) that merges with the `const` value with no value-space collision. The source
`export type X = typeof X.Type` companion is dropped.

Constructor → facade mapping:

| source constructor | emitted base | brand (6th arg) |
|---|---|---|
| `S.Class` / `S.TaggedClass` | `S.OpaqueClassFacade` | `{}` |
| `S.ErrorClass` / `S.TaggedErrorClass` | `S.OpaqueErrorFacadeClass` | `Cause.YieldableError` |
| `S.Opaque` / `S.OpaqueFacade` / requests | `S.OpaqueFacade` | `{}` |
| `S.Struct` / `S.TaggedStruct` | `S.StructFacade` | — (6th arg is `X.Fields`) |

The facade types live in **effect-app** (`>= 4.0.0-beta.279`; the `StructFacade` effect-app-`Struct`
fix lands in the next release). Exact interfaces (effect-app/libs `@`f74ba9e):

- [`OpaqueFacade`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema/Class.ts#L471)
- [`OpaqueClassFacade`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema/Class.ts#L502)
- [`OpaqueErrorFacadeClass`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema/Class.ts#L580)
- [`StructFacade`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema/Class.ts#L654)
- [`EnhancedClass`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema/Class.ts#L17) (the stock class base we rewrite away)
- [effect-app `Struct`](https://github.com/effect-app/libs/blob/f74ba9e6b6805010ec2ff54dd7db119209ea5521/packages/effect-app/src/Schema.ts#L204) (the base `StructFacade` extends — effect-app's own, not effect core's)

---

## Why

Effect schemas encode their decoded/encoded/make/services views as type-level fields computed
from `fields`. Without expansion, every consumer that touches a model re-instantiates those
generics (and risks depth-limit blowups that silently infer `any`/`unknown`).

The in-source benefit is two distinct things:

1. **Named view interfaces** — `Encoded`/`Make`/`Type` materialized as literal interfaces. On its
own only ~24% of the win.
2. **Class-base facade** — dropping the `S.Struct<{…full…}>` from the model's emitted base. This
is **~76%** of the win.

Doing both at `.d.ts` emit means: codegen is purely text-based again (no type checker in the
lint/codegen path), source stays clean, and the expansion happens once per model regardless of how
many consumers read it.

The whole `S.Bottom` surface is pinned by the facade type, so consumers never re-derive *any*
Bottom field from `fields` — `Type`/`Encoded`/`Make`/`DecodingServices`/`EncodingServices` resolve
to the named namespace interfaces; `ast`/`Rebuild`/`Iso`/`~type.parameters`/variance markers are
fixed to constants/`Self`.

### Why the win compounds — there is no cross-program instantiation cache

TypeScript's type-instantiation cache is **per `Program`**, not global. Every `Program` re-checks
the `.d.ts` of its dependencies from scratch and re-instantiates each schema generic it touches.
So the dynamic-type cost is paid **once per program that sees the model**, and a codebase multiplies
programs:

- **Project references** — each referenced project is its own `Program`; a model in a shared package
is re-instantiated in every downstream project that imports it.
- **Multi-threaded checking** — each parallel checker has its own cache (tsgo multi ≈ 2.2× the
instantiations of single-threaded; §2a), so the cost is paid per worker too.
- **Editor + `tsc` + build + test configs** — each is another program over the same files.

A stock model's base inlines `S.Struct<{…full…}>` and its `Encoded`/`Type` are conditional, so
**N programs = N re-derivations** of the same shapes. The facade collapses the model to named
interfaces materialized **once** at emit; each program then reads cheap literals by name. Hence the
saving **scales with the number of programs / project references / parallel checkers** — the more of
them, the larger the absolute win. A monorepo with many project references is the worst case for
stock and the best case for the facade.

### Not just performance — correctness

This is **also a correctness fix**, not only a speed-up. When the schema generics get deep/complex
enough, the checker hits its instantiation/depth limits and **silently** falls back to `unknown` /
`any` for whole views — `DecodingServices` / `EncodingServices`, constructor / `make` members, the
decoded `Type` and `Encoded`. There is **no error**: the program type-checks green against a degraded
type. In large projects this is worse, and it is **non-deterministic** — it depends on checker /
project / worker state, so it shows up most under **tsgo's default multi-threaded mode** (each worker
hits the wall independently) and can differ run-to-run.

Because the facade materializes every view as a **named literal interface once at emit**, consumers
read fully-resolved types instead of re-deriving (and giving up on) them — the views are stable and
correct regardless of program count or worker. Switching the scanner onto this surfaced **several
real bugs** that the silent `any`/`unknown` had been masking (missing required services, wrong
make-input shapes, fields that had silently widened). So the facade both cuts instantiations and
**removes a class of silent, non-deterministic type degradations**.

---

## How

Three compiler files (kept minimal so the diff re-applies as a patch onto the Effect-TS forks):

- `src/compiler/types.ts` — add the `createTypeOfStructSchemaProperty` `EmitResolver` signature.
- `src/compiler/checker.ts` — implement the resolver methods that turn a class-schema property or a
struct const's initializer property into a serialized `TypeNode`
(`createTypeOfClassStaticProperty`, `createMakeTypeOfClassDeclaration`,
`createTypeLiteralOfClassDeclaration`, and **`createTypeOfStructSchemaProperty`**). All go through
`nodeBuilder.typeToTypeNode(… | UseFullyQualifiedType | MultilineObjectLiterals)`.
- `src/compiler/transformers/declarations.ts` — the transform
(`createEffectSchemaSourceFileDeclarations` and helpers).

Key mechanics:

- **Materialization reads the source, serializes the resolved type.** Class members come off the
class heritage's schema expression; struct members come off the const's initializer
(`createTypeOfStructSchemaProperty`). Serializing the *resolved* type (not a synthesized
`S.Struct.*` / indexed-access reference) keeps `never` services as `never` and avoids checker
flow/position crashes.
- **`identifier` is NOT compiler-emitted.** It is a generic `string` `S.Class` static, so it lives
on the class facade interfaces (`OpaqueClassFacade`/`OpaqueErrorFacadeClass`) in effect-app — not
on `OpaqueFacade` (`S.Opaque`/requests build on `S.Bottom`, not `S.Class`, so they have none).
Only per-model, precisely-typed statics (`fields`/`mapFields`/`to`/`from`/`copy`) are emitted by
the compiler.
- **Struct stays a `const`.** A type-only `declare namespace X` has no value side, so it merges with
the struct `const` — no fake class needed.
- **`StructFacade` is Workflow-compatible.** It `Omit`s the overridden type-level keys off
effect-app's `Struct<Fields>` and re-pins them, so the value is still a real (effect-app) struct
schema: `Workflow.AnyStructSchema`, the `Struct<Fields & Context>` reconstruction, `Union`,
`.fields.x` all keep working. (`OpaqueFacade` is *not* a `Struct` and breaks workflows — that's
why structs need their own facade.)

### Gotchas

- `NodeBuilderFlags.UseFullyQualifiedType` is mandatory — without it nested model refs collapse to
the bare sibling name (`address: Encoded` not `address: Address.Encoded`).
- `never` services materialize to `{}` if you go through `createTypeLiteralOfTypeNode`; serialize
the resolved type instead so `never` stays `never`.
- `StructFacade` must extend **effect-app's** `Struct`, not `effect/Schema`'s — they are distinct
types and the core one is not assignable where an effect-app struct is expected.

---

## Results

Scanner `api` `--build` (`tsconfig.src.json --force`, 13 projects, **0 errors**, identical source),
stock tsc 6.0.3 vs the final patched tsc:

| metric | stock tsc 6.0.3 | final patched | Δ |
|---|--:|--:|--:|
| **Type instantiations** | 15,704,767 | **11,077,561** | **−29.5%** |
| Types | 2,178,240 | 1,901,507 | −12.7% |
| Aggregate check time | 21.50s | 17.71s | −17.6% |

112 models faceted: **56 `StructFacade` + 46 `OpaqueErrorFacadeClass` + 10 `OpaqueClassFacade`**.
The Go compiler emits byte-identical facades (parity verified on the same tree).

The change also composes with Effect's tooling:
- **effect-app/tsgo** — re-expressed as `_patches/029-031`; the patched binary runs the scanner
with **0 errors + Effect LSP diagnostics (hooks live) + our facades (emitter live)**.
- **`@effect/language-service`** — its build-time `_tsc.js` patch injects at `emitFilesAndReportErrors`
(disjoint from our declaration-transformer change), so the two compose; `effect-language-service patch`
applies cleanly on top of our patched compiler.

---

## Related

- [effect-app/TypeScript#3](https://github.com/effect-app/TypeScript/pull/3) (this fork) — Go mirror [effect-app/typescript-go#2](https://github.com/effect-app/typescript-go/pull/2); patch form [effect-app/tsgo#1](https://github.com/effect-app/tsgo/pull/1).
- [effect-app/libs#801](https://github.com/effect-app/libs/pull/801) (facade `identifier` + `StructFacade`) + the `StructFacade` effect-app-`Struct` fix on [`main`](https://github.com/effect-app/libs/commits/main).
- Full plan / measurements / fork-layering: scanner [macs-holding/scanner#1597](https://github.com/macs-holding/scanner/pull/1597) (`docs/planning/dts-emit-schema-codegen.md`).
1 change: 1 addition & 0 deletions lib/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text eol=lf
Loading
Loading