# Newtype

A **newtype** is a purely compile-time wrapper around a *carrier* type (the underlying primitive or object). It prevents you from accidentally mixing structurally identical values — a `UserId` and an `OrderId` are both `number` at runtime, but the type system keeps them apart. Unlike a [`Brand`](https://effect.plants.sh/data-types/branded-types/), which is an intersection (`number & Brand<"UserId">`) and so still flows wherever a plain `number` is expected, a newtype is **fully opaque**: a `Newtype<"UserId", number>` cannot be used where a `number` is required until you explicitly unwrap it.

Three terms describe a newtype:

- **Carrier** — the underlying type being wrapped (e.g. `string`, `number`).
- **Key** — a unique string literal that distinguishes one newtype from another (e.g. `"UserId"`, `"OrderId"`).
- **Iso** — a lossless two-way conversion between a newtype and its carrier, created with `Newtype.makeIso`.

There is **zero runtime overhead**: every operation in this module is an identity cast. The tag exists only in the type system.

## Defining a newtype and an iso

Define a newtype by declaring an `interface` that extends `Newtype.Newtype<Key, Carrier>`. Then build an iso to wrap (`set`) and unwrap (`get`) values.

```ts
import { Newtype } from "effect"

// 1. Define a newtype: key "Label", carrier `string`
interface Label extends Newtype.Newtype<"Label", string> {}

// 2. Create an iso for wrapping / unwrapping
const labelIso = Newtype.makeIso<Label>()

// 3. Wrap a raw string into the newtype
const myLabel: Label = labelIso.set("hello")

// 4. Unwrap back to the carrier
const raw: string = labelIso.get(myLabel)
// => "hello"
```

The iso returned by `makeIso` is an `Optic.Iso`, so it composes with the rest of the `Optic` API (lenses, prisms) when a newtype sits inside a larger structure.

## Unwrapping only

If you only need to read the inner value and never wrap new ones, use `Newtype.value` — it is the `get` direction of the iso on its own.

```ts
import { Newtype } from "effect"

interface Label extends Newtype.Newtype<"Label", string> {}

const iso = Newtype.makeIso<Label>()
const label = iso.set("hello")

const raw: string = Newtype.value(label)
// => "hello"
```

## Keys must be unique

Two newtypes that share the same key string are assignable to each other, because the tag is keyed entirely on that literal. Always choose a distinct key per newtype.

```ts
import { Newtype } from "effect"

interface UserId extends Newtype.Newtype<"UserId", number> {}
interface OrderId extends Newtype.Newtype<"OrderId", number> {}

const userIso = Newtype.makeIso<UserId>()
const orderIso = Newtype.makeIso<OrderId>()

const user: UserId = userIso.set(1)

// const order: OrderId = user
//                        ^ Type error: 'UserId' is not assignable to 'OrderId'
```

:::caution[Same key, same type]
`Newtype<"Id", number>` and `Newtype<"Id", number>` declared in different files are the **same** newtype as far as TypeScript is concerned. Keep keys globally unique (prefix with a module or domain name if collisions are a risk). A newtype is also **not** assignable to its carrier without unwrapping via `value` or an iso.
:::

## Newtype vs Brand

Both [`Brand`](https://effect.plants.sh/data-types/branded-types/) and `Newtype` give a primitive a distinct identity, but they make different tradeoffs:

| | `Brand` | `Newtype` |
| --- | --- | --- |
| Relationship to carrier | Intersection: `number & Brand<"Id">` — still usable *as* a `number` | Opaque: not assignable to the carrier without unwrapping |
| Runtime validation | Yes — `Brand.make` / `Brand.check` can validate and reject values (`Brand.nominal` skips it) | No — purely structural, no validation |
| Unwrapping | Not needed; the value already *is* the carrier | Required via `value` / `makeIso` |
| Lifting trait instances | Manual | Built-in helpers (`makeEquivalence`, `makeOrder`, `makeCombiner`, `makeReducer`) |

Choose **`Brand`** when you want validated, smart-constructor values that remain interchangeable with the base type. Choose **`Newtype`** when you want a strictly opaque wrapper that the compiler refuses to confuse with the raw carrier, and you want to lift `Equivalence`/`Order`/`Combiner`/`Reducer` instances onto it cheaply.

## Reference

### Newtype.Newtype

The tagged interface. Extend it as an `interface` to declare a newtype with a unique key and a carrier type. The tag is compile-time only — no runtime wrapper is allocated.

```ts
import { Newtype } from "effect"

interface UserId extends Newtype.Newtype<"UserId", number> {}
interface OrderId extends Newtype.Newtype<"OrderId", number> {}
// Both wrap `number`, but are not assignable to each other.
```

### Newtype.Any

The type that matches any newtype. Use it as a generic constraint when a type parameter can be any newtype: `<N extends Newtype.Any>`.

```ts
import { Newtype } from "effect"

// Accepts any newtype as the type parameter
declare function describe<N extends Newtype.Any>(value: N): string
// => N is constrained to `Newtype<any, unknown>`
```

### Newtype.Key

A type-level helper that extracts the key literal from a newtype.

```ts
import { Newtype } from "effect"

interface UserId extends Newtype.Newtype<"UserId", number> {}

type K = Newtype.Key<UserId>
// => "UserId"
```

### Newtype.Carrier

A type-level helper that extracts the carrier (underlying) type from a newtype.

```ts
import { Newtype } from "effect"

interface UserId extends Newtype.Newtype<"UserId", number> {}

type C = Newtype.Carrier<UserId>
// => number
```

### Newtype.makeIso

Creates an `Optic.Iso<N, Carrier>` for a newtype, giving both wrapping (`set`) and unwrapping (`get`). Both directions are identity casts, so there is no runtime cost. Because the result is an `Optic.Iso`, it composes with other optics.

```ts
import { Newtype } from "effect"

interface Label extends Newtype.Newtype<"Label", string> {}

const labelIso = Newtype.makeIso<Label>()

const label: Label = labelIso.set("world")
// => Label wrapping "world"

const str: string = labelIso.get(label)
// => "world"
```

### Newtype.value

Unwraps a newtype value, returning the underlying carrier. This is an identity cast (zero runtime cost). Prefer `makeIso` when you also need to wrap values.

```ts
import { Newtype } from "effect"

interface Label extends Newtype.Newtype<"Label", string> {}

const iso = Newtype.makeIso<Label>()
const label = iso.set("hello")

const raw: string = Newtype.value(label)
// => "hello"
```

### Newtype.makeEquivalence

Lifts an `Equivalence<Carrier>` into an `Equivalence<Newtype>`, so you can compare two newtype values for equality. The result delegates to the carrier equivalence.

```ts
import { Equivalence, Newtype } from "effect"

interface Label extends Newtype.Newtype<"Label", string> {}

const eq = Newtype.makeEquivalence<Label>(Equivalence.String)
const iso = Newtype.makeIso<Label>()

eq(iso.set("a"), iso.set("a"))
// => true

eq(iso.set("a"), iso.set("b"))
// => false
```

See also: [Equivalence](https://effect.plants.sh/traits/equivalence/).

### Newtype.makeOrder

Lifts an `Order<Carrier>` into an `Order<Newtype>`, so you can sort or compare newtype values. The result delegates to the carrier order and returns `-1`, `0`, or `1`.

```ts
import { Newtype, Order } from "effect"

interface Score extends Newtype.Newtype<"Score", number> {}

const ord = Newtype.makeOrder<Score>(Order.Number)
const iso = Newtype.makeIso<Score>()

ord(iso.set(1), iso.set(2))
// => -1

ord(iso.set(2), iso.set(2))
// => 0
```

See also: [Order](https://effect.plants.sh/traits/order/).

### Newtype.makeCombiner

Lifts a `Combiner<Carrier>` into a `Combiner<Newtype>`. Use it to merge two newtype values; the result delegates to the carrier combiner's `combine`.

```ts
import { Combiner, Newtype } from "effect"

interface Amount extends Newtype.Newtype<"Amount", number> {}

const sum = Combiner.make<number>((a, b) => a + b)
const combiner = Newtype.makeCombiner<Amount>(sum)
const iso = Newtype.makeIso<Amount>()

const total = combiner.combine(iso.set(10), iso.set(20))
Newtype.value(total)
// => 30
```

### Newtype.makeReducer

Lifts a `Reducer<Carrier>` into a `Reducer<Newtype>`. A `Reducer` adds an `initialValue` and `combineAll` to a `Combiner`, so you can fold an entire collection of newtype values.

```ts
import { Newtype, Reducer } from "effect"

interface Score extends Newtype.Newtype<"Score", number> {}

const sum = Reducer.make<number>((a, b) => a + b, 0)
const reducer = Newtype.makeReducer<Score>(sum)
const iso = Newtype.makeIso<Score>()

const total = reducer.combineAll([iso.set(1), iso.set(2), iso.set(3)])
Newtype.value(total)
// => 6

// combineAll on an empty collection returns the initialValue (wrapped)
Newtype.value(reducer.combineAll([]))
// => 0
```

## See also

- [Branded Types](https://effect.plants.sh/data-types/branded-types/) — the validating, carrier-compatible alternative.
- [Equivalence](https://effect.plants.sh/traits/equivalence/) — building equality instances to lift with `makeEquivalence`.
- [Order](https://effect.plants.sh/traits/order/) — building ordering instances to lift with `makeOrder`.