Skip to content

Commit ff59c8b

Browse files
committed
Experimental: make sum safer, closes #523
1 parent 46999ba commit ff59c8b

28 files changed

+174
-64
lines changed

CHANGELOG.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,35 @@
1414
**Note**: Gaps between patch versions are faulty/broken releases.
1515
**Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.
1616

17+
# 2.2.12
18+
19+
- **Experimental**
20+
- (\*) make `sum` safer, closes #523 (@gcanti)
21+
22+
(\*) breaking change
23+
24+
In case of non-`string` tag values, the respective key must be enclosed in brackets
25+
26+
```ts
27+
export const MySum: D.Decoder<
28+
unknown,
29+
| {
30+
type: 1 // non-`string` tag value
31+
a: string
32+
}
33+
| {
34+
type: 2 // non-`string` tag value
35+
b: number
36+
}
37+
> = D.sum('type')({
38+
[1]: D.type({ type: D.literal(1), a: D.string }),
39+
[2]: D.type({ type: D.literal(2), b: D.number })
40+
})
41+
```
42+
1743
# 2.2.11
1844

19-
- **Polish**
45+
- **Experimental**
2046
- `Decoder`
2147
- make `toForest` stack-safe, #520 (@safareli)
2248

Decoder.md

+21
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ export const MySum: D.Decoder<
224224
})
225225
```
226226

227+
**non-`string` tag values**
228+
229+
In case of non-`string` tag values, the respective key must be enclosed in brackets
230+
231+
```ts
232+
export const MySum: D.Decoder<
233+
unknown,
234+
| {
235+
type: 1 // non-`string` tag value
236+
a: string
237+
}
238+
| {
239+
type: 2 // non-`string` tag value
240+
b: number
241+
}
242+
> = D.sum('type')({
243+
[1]: D.type({ type: D.literal(1), a: D.string }),
244+
[2]: D.type({ type: D.literal(2), b: D.number })
245+
})
246+
```
247+
227248
## The `union` combinator
228249

229250
The `union` combinator describes untagged unions

docs/modules/Decoder.ts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ Added in v2.2.7
358358
```ts
359359
export declare const sum: <T extends string>(
360360
tag: T
361-
) => <A>(members: { [K in keyof A]: Decoder<unknown, A[K]> }) => Decoder<unknown, A[keyof A]>
361+
) => <A>(members: { [K in keyof A]: Decoder<unknown, A[K] & Record<T, K>> }) => Decoder<unknown, A[keyof A]>
362362
```
363363
364364
Added in v2.2.7

docs/modules/Eq.ts.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ Added in v2.2.2
112112
**Signature**
113113

114114
```ts
115-
export declare function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Eq<A[K]> }) => Eq<A[keyof A]>
115+
export declare function sum<T extends string>(
116+
tag: T
117+
): <A>(members: { [K in keyof A]: Eq<A[K] & Record<T, K>> }) => Eq<A[keyof A]>
116118
```
117119

118120
Added in v2.2.2

docs/modules/Guard.ts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ Added in v2.2.0
171171
```ts
172172
export declare const sum: <T extends string>(
173173
tag: T
174-
) => <A>(members: { [K in keyof A]: Guard<unknown, A[K]> }) => Guard<unknown, A[keyof A]>
174+
) => <A>(members: { [K in keyof A]: Guard<unknown, A[K] & Record<T, K>> }) => Guard<unknown, A[keyof A]>
175175
```
176176
177177
Added in v2.2.0

docs/modules/Schemable.ts.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ export interface Schemable<S> {
6767
readonly array: <A>(item: HKT<S, A>) => HKT<S, Array<A>>
6868
readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: HKT<S, A[K]> }) => HKT<S, A>
6969
readonly intersect: <B>(right: HKT<S, B>) => <A>(left: HKT<S, A>) => HKT<S, A & B>
70-
readonly sum: <T extends string>(tag: T) => <A>(members: { [K in keyof A]: HKT<S, A[K]> }) => HKT<S, A[keyof A]>
70+
readonly sum: <T extends string>(
71+
tag: T
72+
) => <A>(members: { [K in keyof A]: HKT<S, A[K] & Record<T, K>> }) => HKT<S, A[keyof A]>
7173
readonly lazy: <A>(id: string, f: () => HKT<S, A>) => HKT<S, A>
7274
}
7375
```
@@ -92,7 +94,9 @@ export interface Schemable1<S extends URIS> {
9294
readonly array: <A>(item: Kind<S, A>) => Kind<S, Array<A>>
9395
readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
9496
readonly intersect: <B>(right: Kind<S, B>) => <A>(left: Kind<S, A>) => Kind<S, A & B>
95-
readonly sum: <T extends string>(tag: T) => <A>(members: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A[keyof A]>
97+
readonly sum: <T extends string>(
98+
tag: T
99+
) => <A>(members: { [K in keyof A]: Kind<S, A[K] & Record<T, K>> }) => Kind<S, A[keyof A]>
96100
readonly lazy: <A>(id: string, f: () => Kind<S, A>) => Kind<S, A>
97101
}
98102
```
@@ -123,7 +127,7 @@ export interface Schemable2C<S extends URIS2, E> {
123127
readonly intersect: <B>(right: Kind2<S, E, B>) => <A>(left: Kind2<S, E, A>) => Kind2<S, E, A & B>
124128
readonly sum: <T extends string>(
125129
tag: T
126-
) => <A>(members: { [K in keyof A]: Kind2<S, E, A[K]> }) => Kind2<S, E, A[keyof A]>
130+
) => <A>(members: { [K in keyof A]: Kind2<S, E, A[K] & Record<T, K>> }) => Kind2<S, E, A[keyof A]>
127131
readonly lazy: <A>(id: string, f: () => Kind2<S, E, A>) => Kind2<S, E, A>
128132
}
129133
```

docs/modules/TaskDecoder.ts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ Added in v2.2.7
361361
```ts
362362
export declare const sum: <T extends string>(
363363
tag: T
364-
) => <A>(members: { [K in keyof A]: TaskDecoder<unknown, A[K]> }) => TaskDecoder<unknown, A[keyof A]>
364+
) => <A>(members: { [K in keyof A]: TaskDecoder<unknown, A[K] & Record<T, K>> }) => TaskDecoder<unknown, A[keyof A]>
365365
```
366366
367367
Added in v2.2.7

docs/modules/Type.ts.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Added in v2.2.3
130130
```ts
131131
export declare const sum: <T extends string>(
132132
_tag: T
133-
) => <A>(members: { [K in keyof A]: Type<A[K]> }) => Type<A[keyof A]>
133+
) => <A>(members: { [K in keyof A]: Type<A[K] & Record<T, K>> }) => Type<A[keyof A]>
134134
```
135135
136136
Added in v2.2.3

dtslint/ts3.5/Codec.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ _.fromSum('_tag')({
106106
// sum
107107
//
108108

109+
const S1 = _.type({ _tag: _.literal('A'), a: _.string })
110+
const S2 = _.type({ _tag: _.literal('B'), b: _.number })
111+
109112
// $ExpectType Codec<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
110-
_.sum('_tag')({
111-
A: _.type({ _tag: _.literal('A'), a: _.string }),
112-
B: _.type({ _tag: _.literal('B'), b: _.number })
113-
})
113+
_.sum('_tag')({ A: S1, B: S2 })
114+
// // $ExpectError
115+
// _.sum('_tag')({ A: S1, B: S1 })

dtslint/ts3.5/Decoder.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,13 @@ _.fromSum('_tag')({
123123
// sum
124124
//
125125

126+
const S1 = _.type({ _tag: _.literal('A'), a: _.string })
127+
const S2 = _.type({ _tag: _.literal('B'), b: _.number })
128+
126129
// $ExpectType Decoder<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
127-
_.sum('_tag')({
128-
A: _.type({ _tag: _.literal('A'), a: _.string }),
129-
B: _.type({ _tag: _.literal('B'), b: _.number })
130-
})
130+
_.sum('_tag')({ A: S1, B: S2 })
131+
// $ExpectError
132+
_.sum('_tag')({ A: S1, B: S1 })
131133

132134
//
133135
// union

dtslint/ts3.5/Eq.ts

+12
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,15 @@ _.partial({
1515
c: _.number
1616
})
1717
})
18+
19+
//
20+
// sum
21+
//
22+
23+
const S1 = _.type({ _tag: _.Schemable.literal('A'), a: _.string })
24+
const S2 = _.type({ _tag: _.Schemable.literal('B'), b: _.number })
25+
26+
// $ExpectType Eq<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
27+
_.sum('_tag')({ A: S1, B: S2 })
28+
// // $ExpectError
29+
// _.sum('_tag')({ A: S1, B: S1 })

dtslint/ts3.5/Guard.ts

+12
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,15 @@ _.partial({
3030

3131
// $ExpectType { a: string; b: { c: number; }; }
3232
export type A = _.TypeOf<typeof A>
33+
34+
//
35+
// sum
36+
//
37+
38+
const S1 = _.type({ _tag: _.literal('A'), a: _.string })
39+
const S2 = _.type({ _tag: _.literal('B'), b: _.number })
40+
41+
// $ExpectType Guard<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
42+
_.sum('_tag')({ A: S1, B: S2 })
43+
// $ExpectError
44+
_.sum('_tag')({ A: S1, B: S1 })

dtslint/ts3.5/Schema.ts

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ const S2 = make((S) => S.type({ _tag: S.literal('B'), b: S.number }))
100100

101101
// $ExpectType Schema<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
102102
make((S) => S.sum('_tag')({ A: S1(S), B: S2(S) }))
103+
// $ExpectError
104+
make((S) => S.sum('_tag')({ A: S1(S), B: S1(S) }))
103105

104106
//
105107
// lazy

dtslint/ts3.5/TaskDecoder.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as _ from '../../src/TaskDecoder'
2+
3+
//
4+
// sum
5+
//
6+
7+
const S1 = _.type({ _tag: _.literal('A'), a: _.string })
8+
const S2 = _.type({ _tag: _.literal('B'), b: _.number })
9+
10+
// $ExpectType TaskDecoder<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
11+
_.sum('_tag')({ A: S1, B: S2 })
12+
// $ExpectError
13+
_.sum('_tag')({ A: S1, B: S1 })

dtslint/ts3.5/tslint.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
"object-literal-shorthand": false,
1515
"prefer-object-spread": false,
1616
"whitespace": false,
17-
"use-default-type-parameter": false
17+
"use-default-type-parameter": false,
18+
"interface-name": false,
19+
"no-promise-as-boolean": false,
20+
"no-eval": false,
21+
"label-position": false,
22+
"function-constructor": false,
23+
"invalid-void": false,
24+
"no-construct": false
1825
}
1926
}

package-lock.json

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "io-ts",
3-
"version": "2.2.11",
3+
"version": "2.2.12",
44
"description": "TypeScript runtime type system for IO decoding/encoding",
55
"main": "lib/index.js",
66
"module": "es6/index.js",
@@ -59,7 +59,7 @@
5959
"ts-node": "8.8.2",
6060
"tslint": "6.1.1",
6161
"tslint-config-standard": "9.0.0",
62-
"typescript": "^3.9.6"
62+
"typescript": "^4.0.3"
6363
},
6464
"tags": [
6565
"typescript",

src/Codec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export function record<O, A>(codomain: Codec<unknown, O, A>): Codec<unknown, Rec
227227
export const fromTuple = <C extends ReadonlyArray<Codec<any, any, any>>>(
228228
...components: C
229229
): Codec<{ [K in keyof C]: InputOf<C[K]> }, { [K in keyof C]: OutputOf<C[K]> }, { [K in keyof C]: TypeOf<C[K]> }> =>
230-
make(D.fromTuple(...components) as any, E.tuple(...components))
230+
make(D.fromTuple(...components) as any, E.tuple(...components)) as any
231231

232232
/**
233233
* @category combinators

src/Decoder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export const record = <A>(codomain: Decoder<unknown, A>): Decoder<unknown, Recor
295295
export const fromTuple = <C extends ReadonlyArray<Decoder<any, any>>>(
296296
...components: C
297297
): Decoder<{ [K in keyof C]: InputOf<C[K]> }, { [K in keyof C]: TypeOf<C[K]> }> =>
298-
K.fromTuple(M)((i, e) => FS.of(DE.index(i, DE.required, e)))(...components)
298+
K.fromTuple(M)((i, e) => FS.of(DE.index(i, DE.required, e)))(...components) as any
299299

300300
/**
301301
* @category combinators
@@ -345,7 +345,7 @@ export const fromSum = <T extends string>(tag: T) => <MS extends Record<string,
345345
* @since 2.2.7
346346
*/
347347
export const sum = <T extends string>(tag: T) => <A>(
348-
members: { [K in keyof A]: Decoder<unknown, A[K]> }
348+
members: { [K in keyof A]: Decoder<unknown, A[K] & Record<T, K>> }
349349
): Decoder<unknown, A[keyof A]> => pipe(UnknownRecord as any, compose(fromSum(tag)(members)))
350350

351351
/**

src/Eq.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ export const intersect = <B>(right: Eq<B>) => <A>(left: Eq<A>): Eq<A & B> => ({
145145
* @category combinators
146146
* @since 2.2.2
147147
*/
148-
export function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Eq<A[K]> }) => Eq<A[keyof A]> {
148+
export function sum<T extends string>(
149+
tag: T
150+
): <A>(members: { [K in keyof A]: Eq<A[K] & Record<T, K>> }) => Eq<A[keyof A]> {
149151
return (members: Record<string, Eq<any>>) => {
150152
return {
151153
equals: (x: Record<string, any>, y: Record<string, any>) => {

src/Guard.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export const union = <A extends readonly [unknown, ...Array<unknown>]>(
218218
* @since 2.2.0
219219
*/
220220
export const sum = <T extends string>(tag: T) => <A>(
221-
members: { [K in keyof A]: Guard<unknown, A[K]> }
221+
members: { [K in keyof A]: Guard<unknown, A[K] & Record<T, K>> }
222222
): Guard<unknown, A[keyof A]> =>
223223
pipe(
224224
UnknownRecord,

src/Kleisli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ export function fromSum<M extends URIS2, E>(
287287
const keys = Object.keys(members)
288288
return {
289289
decode: (ir) => {
290-
const v = ir[tag]
290+
const v: any = ir[tag]
291291
if (v in members) {
292292
return (members as any)[v].decode(ir)
293293
}

0 commit comments

Comments
 (0)