Skip to content

Commit

Permalink
Experimental: make sum safer, closes #523
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Oct 28, 2020
1 parent 46999ba commit ff59c8b
Show file tree
Hide file tree
Showing 28 changed files with 174 additions and 64 deletions.
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,35 @@
**Note**: Gaps between patch versions are faulty/broken releases.
**Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

# 2.2.12

- **Experimental**
- (\*) make `sum` safer, closes #523 (@gcanti)

(\*) breaking change

In case of non-`string` tag values, the respective key must be enclosed in brackets

```ts
export const MySum: D.Decoder<
unknown,
| {
type: 1 // non-`string` tag value
a: string
}
| {
type: 2 // non-`string` tag value
b: number
}
> = D.sum('type')({
[1]: D.type({ type: D.literal(1), a: D.string }),
[2]: D.type({ type: D.literal(2), b: D.number })
})
```

# 2.2.11

- **Polish**
- **Experimental**
- `Decoder`
- make `toForest` stack-safe, #520 (@safareli)

Expand Down
21 changes: 21 additions & 0 deletions Decoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,27 @@ export const MySum: D.Decoder<
})
```

**non-`string` tag values**

In case of non-`string` tag values, the respective key must be enclosed in brackets

```ts
export const MySum: D.Decoder<
unknown,
| {
type: 1 // non-`string` tag value
a: string
}
| {
type: 2 // non-`string` tag value
b: number
}
> = D.sum('type')({
[1]: D.type({ type: D.literal(1), a: D.string }),
[2]: D.type({ type: D.literal(2), b: D.number })
})
```

## The `union` combinator

The `union` combinator describes untagged unions
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/Decoder.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ Added in v2.2.7
```ts
export declare const sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: Decoder<unknown, A[K]> }) => Decoder<unknown, A[keyof A]>
) => <A>(members: { [K in keyof A]: Decoder<unknown, A[K] & Record<T, K>> }) => Decoder<unknown, A[keyof A]>
```
Added in v2.2.7
Expand Down
4 changes: 3 additions & 1 deletion docs/modules/Eq.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ Added in v2.2.2
**Signature**

```ts
export declare function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Eq<A[K]> }) => Eq<A[keyof A]>
export declare function sum<T extends string>(
tag: T
): <A>(members: { [K in keyof A]: Eq<A[K] & Record<T, K>> }) => Eq<A[keyof A]>
```

Added in v2.2.2
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/Guard.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Added in v2.2.0
```ts
export declare const sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: Guard<unknown, A[K]> }) => Guard<unknown, A[keyof A]>
) => <A>(members: { [K in keyof A]: Guard<unknown, A[K] & Record<T, K>> }) => Guard<unknown, A[keyof A]>
```
Added in v2.2.0
Expand Down
10 changes: 7 additions & 3 deletions docs/modules/Schemable.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export interface Schemable<S> {
readonly array: <A>(item: HKT<S, A>) => HKT<S, Array<A>>
readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: HKT<S, A[K]> }) => HKT<S, A>
readonly intersect: <B>(right: HKT<S, B>) => <A>(left: HKT<S, A>) => HKT<S, A & B>
readonly sum: <T extends string>(tag: T) => <A>(members: { [K in keyof A]: HKT<S, A[K]> }) => HKT<S, A[keyof A]>
readonly sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: HKT<S, A[K] & Record<T, K>> }) => HKT<S, A[keyof A]>
readonly lazy: <A>(id: string, f: () => HKT<S, A>) => HKT<S, A>
}
```
Expand All @@ -92,7 +94,9 @@ export interface Schemable1<S extends URIS> {
readonly array: <A>(item: Kind<S, A>) => Kind<S, Array<A>>
readonly tuple: <A extends ReadonlyArray<unknown>>(...components: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A>
readonly intersect: <B>(right: Kind<S, B>) => <A>(left: Kind<S, A>) => Kind<S, A & B>
readonly sum: <T extends string>(tag: T) => <A>(members: { [K in keyof A]: Kind<S, A[K]> }) => Kind<S, A[keyof A]>
readonly sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: Kind<S, A[K] & Record<T, K>> }) => Kind<S, A[keyof A]>
readonly lazy: <A>(id: string, f: () => Kind<S, A>) => Kind<S, A>
}
```
Expand Down Expand Up @@ -123,7 +127,7 @@ export interface Schemable2C<S extends URIS2, E> {
readonly intersect: <B>(right: Kind2<S, E, B>) => <A>(left: Kind2<S, E, A>) => Kind2<S, E, A & B>
readonly sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: Kind2<S, E, A[K]> }) => Kind2<S, E, A[keyof A]>
) => <A>(members: { [K in keyof A]: Kind2<S, E, A[K] & Record<T, K>> }) => Kind2<S, E, A[keyof A]>
readonly lazy: <A>(id: string, f: () => Kind2<S, E, A>) => Kind2<S, E, A>
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/TaskDecoder.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ Added in v2.2.7
```ts
export declare const sum: <T extends string>(
tag: T
) => <A>(members: { [K in keyof A]: TaskDecoder<unknown, A[K]> }) => TaskDecoder<unknown, A[keyof A]>
) => <A>(members: { [K in keyof A]: TaskDecoder<unknown, A[K] & Record<T, K>> }) => TaskDecoder<unknown, A[keyof A]>
```
Added in v2.2.7
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/Type.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Added in v2.2.3
```ts
export declare const sum: <T extends string>(
_tag: T
) => <A>(members: { [K in keyof A]: Type<A[K]> }) => Type<A[keyof A]>
) => <A>(members: { [K in keyof A]: Type<A[K] & Record<T, K>> }) => Type<A[keyof A]>
```
Added in v2.2.3
Expand Down
10 changes: 6 additions & 4 deletions dtslint/ts3.5/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ _.fromSum('_tag')({
// sum
//

const S1 = _.type({ _tag: _.literal('A'), a: _.string })
const S2 = _.type({ _tag: _.literal('B'), b: _.number })

// $ExpectType Codec<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
_.sum('_tag')({
A: _.type({ _tag: _.literal('A'), a: _.string }),
B: _.type({ _tag: _.literal('B'), b: _.number })
})
_.sum('_tag')({ A: S1, B: S2 })
// // $ExpectError
// _.sum('_tag')({ A: S1, B: S1 })
10 changes: 6 additions & 4 deletions dtslint/ts3.5/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,13 @@ _.fromSum('_tag')({
// sum
//

const S1 = _.type({ _tag: _.literal('A'), a: _.string })
const S2 = _.type({ _tag: _.literal('B'), b: _.number })

// $ExpectType Decoder<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
_.sum('_tag')({
A: _.type({ _tag: _.literal('A'), a: _.string }),
B: _.type({ _tag: _.literal('B'), b: _.number })
})
_.sum('_tag')({ A: S1, B: S2 })
// $ExpectError
_.sum('_tag')({ A: S1, B: S1 })

//
// union
Expand Down
12 changes: 12 additions & 0 deletions dtslint/ts3.5/Eq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ _.partial({
c: _.number
})
})

//
// sum
//

const S1 = _.type({ _tag: _.Schemable.literal('A'), a: _.string })
const S2 = _.type({ _tag: _.Schemable.literal('B'), b: _.number })

// $ExpectType Eq<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
_.sum('_tag')({ A: S1, B: S2 })
// // $ExpectError
// _.sum('_tag')({ A: S1, B: S1 })
12 changes: 12 additions & 0 deletions dtslint/ts3.5/Guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ _.partial({

// $ExpectType { a: string; b: { c: number; }; }
export type A = _.TypeOf<typeof A>

//
// sum
//

const S1 = _.type({ _tag: _.literal('A'), a: _.string })
const S2 = _.type({ _tag: _.literal('B'), b: _.number })

// $ExpectType Guard<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
_.sum('_tag')({ A: S1, B: S2 })
// $ExpectError
_.sum('_tag')({ A: S1, B: S1 })
2 changes: 2 additions & 0 deletions dtslint/ts3.5/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ const S2 = make((S) => S.type({ _tag: S.literal('B'), b: S.number }))

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

//
// lazy
Expand Down
13 changes: 13 additions & 0 deletions dtslint/ts3.5/TaskDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as _ from '../../src/TaskDecoder'

//
// sum
//

const S1 = _.type({ _tag: _.literal('A'), a: _.string })
const S2 = _.type({ _tag: _.literal('B'), b: _.number })

// $ExpectType TaskDecoder<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
_.sum('_tag')({ A: S1, B: S2 })
// $ExpectError
_.sum('_tag')({ A: S1, B: S1 })
9 changes: 8 additions & 1 deletion dtslint/ts3.5/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"object-literal-shorthand": false,
"prefer-object-spread": false,
"whitespace": false,
"use-default-type-parameter": false
"use-default-type-parameter": false,
"interface-name": false,
"no-promise-as-boolean": false,
"no-eval": false,
"label-position": false,
"function-constructor": false,
"invalid-void": false,
"no-construct": false
}
}
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "io-ts",
"version": "2.2.11",
"version": "2.2.12",
"description": "TypeScript runtime type system for IO decoding/encoding",
"main": "lib/index.js",
"module": "es6/index.js",
Expand Down Expand Up @@ -59,7 +59,7 @@
"ts-node": "8.8.2",
"tslint": "6.1.1",
"tslint-config-standard": "9.0.0",
"typescript": "^3.9.6"
"typescript": "^4.0.3"
},
"tags": [
"typescript",
Expand Down
2 changes: 1 addition & 1 deletion src/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function record<O, A>(codomain: Codec<unknown, O, A>): Codec<unknown, Rec
export const fromTuple = <C extends ReadonlyArray<Codec<any, any, any>>>(
...components: C
): Codec<{ [K in keyof C]: InputOf<C[K]> }, { [K in keyof C]: OutputOf<C[K]> }, { [K in keyof C]: TypeOf<C[K]> }> =>
make(D.fromTuple(...components) as any, E.tuple(...components))
make(D.fromTuple(...components) as any, E.tuple(...components)) as any

/**
* @category combinators
Expand Down
4 changes: 2 additions & 2 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const record = <A>(codomain: Decoder<unknown, A>): Decoder<unknown, Recor
export const fromTuple = <C extends ReadonlyArray<Decoder<any, any>>>(
...components: C
): Decoder<{ [K in keyof C]: InputOf<C[K]> }, { [K in keyof C]: TypeOf<C[K]> }> =>
K.fromTuple(M)((i, e) => FS.of(DE.index(i, DE.required, e)))(...components)
K.fromTuple(M)((i, e) => FS.of(DE.index(i, DE.required, e)))(...components) as any

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

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Eq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export const intersect = <B>(right: Eq<B>) => <A>(left: Eq<A>): Eq<A & B> => ({
* @category combinators
* @since 2.2.2
*/
export function sum<T extends string>(tag: T): <A>(members: { [K in keyof A]: Eq<A[K]> }) => Eq<A[keyof A]> {
export function sum<T extends string>(
tag: T
): <A>(members: { [K in keyof A]: Eq<A[K] & Record<T, K>> }) => Eq<A[keyof A]> {
return (members: Record<string, Eq<any>>) => {
return {
equals: (x: Record<string, any>, y: Record<string, any>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/Guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export const union = <A extends readonly [unknown, ...Array<unknown>]>(
* @since 2.2.0
*/
export const sum = <T extends string>(tag: T) => <A>(
members: { [K in keyof A]: Guard<unknown, A[K]> }
members: { [K in keyof A]: Guard<unknown, A[K] & Record<T, K>> }
): Guard<unknown, A[keyof A]> =>
pipe(
UnknownRecord,
Expand Down
2 changes: 1 addition & 1 deletion src/Kleisli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export function fromSum<M extends URIS2, E>(
const keys = Object.keys(members)
return {
decode: (ir) => {
const v = ir[tag]
const v: any = ir[tag]
if (v in members) {
return (members as any)[v].decode(ir)
}
Expand Down
Loading

0 comments on commit ff59c8b

Please sign in to comment.