Skip to content

Commit e32e2ff

Browse files
fix: 🐛 tuple and map key immutability
1 parent d61f204 commit e32e2ff

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

packages/immutable/src/index.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
export type Immutable<T> = T extends Array<infer E>
2-
? ImmutableArray<E>
3-
: T extends Map<infer K, infer V>
4-
? ImmutableMap<K, V>
5-
: T extends Set<infer E>
6-
? ImmutableSet<E>
7-
: T extends Record<string, infer V>
8-
? ImmutableObject<T, V>
9-
: T
1+
export type Immutable<T> = T extends readonly [infer First, ...infer Rest]
2+
? readonly [Immutable<First>, ...ImmutableTuple<Rest>]
3+
: T extends readonly (infer E)[]
4+
? ReadonlyArray<Immutable<E>>
5+
: T extends Map<infer K, infer V>
6+
? ReadonlyMap<Immutable<K>, Immutable<V>>
7+
: T extends Set<infer E>
8+
? ReadonlySet<Immutable<E>>
9+
: T extends Record<string, infer V>
10+
? { readonly [P in keyof T]: Immutable<V> }
11+
: T
1012

11-
type ImmutableArray<T> = ReadonlyArray<Immutable<T>>
12-
type ImmutableObject<T, V> = { readonly [P in keyof T]: Immutable<V> }
13-
type ImmutableMap<K, V> = ReadonlyMap<K, Immutable<V>>
14-
type ImmutableSet<T> = ReadonlySet<Immutable<T>>
13+
// biome-ignore lint/suspicious/noExplicitAny: extend tuples of any type
14+
export type ImmutableTuple<T extends readonly any[]> = T extends readonly [
15+
infer F,
16+
...infer R,
17+
]
18+
? readonly [Immutable<F>, ...ImmutableTuple<R>]
19+
: readonly []
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
import type { Immutable } from '../src/index'
3+
4+
describe('Immutable', () => {
5+
it('marks nested object properties as readonly', () => {
6+
type Actual = Immutable<{ foo: { bar: string } }>
7+
type Expected = { readonly foo: { readonly bar: string } }
8+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
9+
})
10+
11+
it('transforms arrays into readonly arrays', () => {
12+
type Actual = Immutable<number[]>
13+
type Expected = readonly number[]
14+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
15+
})
16+
17+
it('keeps tuple structure while wrapping entries', () => {
18+
type Actual = Immutable<[number, { label: string }]>
19+
type Expected = readonly [number, Readonly<{ label: string }>]
20+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
21+
})
22+
23+
it('wraps map values with immutable variants', () => {
24+
type Actual = Immutable<Map<{ key: string }, { value: number }>>
25+
type Expected = ReadonlyMap<
26+
Readonly<{ key: string }>,
27+
Readonly<{ value: number }>
28+
>
29+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
30+
})
31+
32+
it('wraps set values with immutable variants', () => {
33+
type Actual = Immutable<Set<{ id: string }>>
34+
type Expected = ReadonlySet<Readonly<{ id: string }>>
35+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
36+
})
37+
38+
it('leaves primitive values unchanged', () => {
39+
type Actual = Immutable<string>
40+
type Expected = string
41+
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
42+
})
43+
})

0 commit comments

Comments
 (0)