Skip to content

Commit c8a6c15

Browse files
committed
fix record domain handling, closes gcanti#391
1 parent ed26ab5 commit c8a6c15

File tree

5 files changed

+321
-155
lines changed

5 files changed

+321
-155
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
**Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a
1515
high state of flux, you're at risk of it changing without notice.
1616

17+
# 2.1.1
18+
19+
- **Bug Fix**
20+
- fix `record` domain handling, closes #391 (@gcanti)
21+
1722
# 2.1.0
1823

1924
- **New Feature**

docs/modules/index.ts.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,11 +1882,7 @@ Added in v1.0.0
18821882
**Signature**
18831883
18841884
```ts
1885-
export const record = <D extends Mixed, C extends Mixed>(
1886-
domain: D,
1887-
codomain: C,
1888-
name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
1889-
): RecordC<D, C> => ...
1885+
export function record<D extends Mixed, C extends Mixed>(domain: D, codomain: C, name?: string): RecordC<D, C> { ... }
18901886
```
18911887
18921888
Added in v1.7.1

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "io-ts",
3-
"version": "2.1.0",
3+
"version": "2.1.1",
44
"description": "TypeScript compatible runtime type system for IO validation",
55
"files": [
66
"lib",

src/index.ts

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,6 @@ const pushAll = <A>(xs: Array<A>, ys: Array<A>): void => {
213213
}
214214
}
215215

216-
const getIsCodec = <T extends Any>(tag: string) => (codec: Any): codec is T => (codec as any)._tag === tag
217-
218-
// tslint:disable-next-line: deprecation
219-
const isAnyCodec = getIsCodec<AnyType>('AnyType')
220-
221-
const isInterfaceCodec = getIsCodec<InterfaceType<Props>>('InterfaceType')
222-
223-
const isPartialCodec = getIsCodec<PartialType<Props>>('PartialType')
224-
225216
//
226217
// basic types
227218
//
@@ -999,21 +990,80 @@ export type OutputOfDictionary<D extends Any, C extends Any> = { [K in OutputOf<
999990
export interface RecordC<D extends Mixed, C extends Mixed>
1000991
extends DictionaryType<D, C, { [K in TypeOf<D>]: TypeOf<C> }, { [K in OutputOf<D>]: OutputOf<C> }, unknown> {}
1001992

993+
function enumerableRecord<D extends Mixed, C extends Mixed>(
994+
keys: Array<string>,
995+
domain: D,
996+
codomain: C,
997+
name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
998+
): RecordC<D, C> {
999+
const len = keys.length
1000+
return new DictionaryType(
1001+
name,
1002+
(u): u is { [K in TypeOf<D>]: TypeOf<C> } => UnknownRecord.is(u) && keys.every(k => codomain.is(u[k])),
1003+
(u, c) =>
1004+
chain(UnknownRecord.validate(u, c), o => {
1005+
const a: { [key: string]: any } = {}
1006+
const errors: Errors = []
1007+
let changed: boolean = false
1008+
for (let i = 0; i < len; i++) {
1009+
const k = keys[i]
1010+
const ok = o[k]
1011+
const codomainResult = codomain.validate(ok, appendContext(c, k, codomain, ok))
1012+
if (isLeft(codomainResult)) {
1013+
pushAll(errors, codomainResult.left)
1014+
} else {
1015+
const vok = codomainResult.right
1016+
changed = changed || vok !== ok
1017+
a[k] = vok
1018+
}
1019+
}
1020+
return errors.length > 0 ? failures(errors) : success((changed || Object.keys(o).length !== len ? a : o) as any)
1021+
}),
1022+
codomain.encode === identity
1023+
? identity
1024+
: (a: any) => {
1025+
const s: { [key: string]: any } = {}
1026+
for (let i = 0; i < len; i++) {
1027+
const k = keys[i]
1028+
s[k] = codomain.encode(a[k])
1029+
}
1030+
return s as any
1031+
},
1032+
domain,
1033+
codomain
1034+
)
1035+
}
1036+
10021037
/**
1003-
* @since 1.7.1
1038+
* @internal
10041039
*/
1005-
export const record = <D extends Mixed, C extends Mixed>(
1040+
export function getDomainKeys<D extends Mixed>(domain: D): Record<string, unknown> | undefined {
1041+
if (isLiteralC(domain)) {
1042+
const literal = domain.value
1043+
if (string.is(literal)) {
1044+
return { [literal]: null }
1045+
}
1046+
} else if (isKeyofC(domain)) {
1047+
return domain.keys
1048+
} else if (isUnionC(domain)) {
1049+
const keys = domain.types.map(type => getDomainKeys(type))
1050+
return keys.some(undefinedType.is) ? undefined : Object.assign({}, ...keys)
1051+
}
1052+
return undefined
1053+
}
1054+
1055+
function nonEnumerableRecord<D extends Mixed, C extends Mixed>(
10061056
domain: D,
10071057
codomain: C,
10081058
name: string = `{ [K in ${domain.name}]: ${codomain.name} }`
1009-
): RecordC<D, C> => {
1059+
): RecordC<D, C> {
10101060
return new DictionaryType(
10111061
name,
10121062
(u): u is { [K in TypeOf<D>]: TypeOf<C> } => {
10131063
if (UnknownRecord.is(u)) {
10141064
return Object.keys(u).every(k => domain.is(k) && codomain.is(u[k]))
10151065
}
1016-
return isAnyCodec(codomain) && Array.isArray(u)
1066+
return isAnyC(codomain) && Array.isArray(u)
10171067
},
10181068
(u, c) => {
10191069
if (UnknownRecord.is(u)) {
@@ -1044,7 +1094,7 @@ export const record = <D extends Mixed, C extends Mixed>(
10441094
}
10451095
return errors.length > 0 ? failures(errors) : success((changed ? a : u) as any)
10461096
}
1047-
if (isAnyCodec(codomain) && Array.isArray(u)) {
1097+
if (isAnyC(codomain) && Array.isArray(u)) {
10481098
return success(u)
10491099
}
10501100
return failure(u, c)
@@ -1066,6 +1116,16 @@ export const record = <D extends Mixed, C extends Mixed>(
10661116
)
10671117
}
10681118

1119+
/**
1120+
* @since 1.7.1
1121+
*/
1122+
export function record<D extends Mixed, C extends Mixed>(domain: D, codomain: C, name?: string): RecordC<D, C> {
1123+
const keys = getDomainKeys(domain)
1124+
return keys
1125+
? enumerableRecord(Object.keys(keys), domain, codomain, name)
1126+
: nonEnumerableRecord(domain, codomain, name)
1127+
}
1128+
10691129
/**
10701130
* @since 1.0.0
10711131
*/
@@ -1628,9 +1688,9 @@ const stripKeys = (o: any, props: Props): unknown => {
16281688
}
16291689

16301690
const getExactTypeName = (codec: Any): string => {
1631-
if (isInterfaceCodec(codec)) {
1691+
if (isTypeC(codec)) {
16321692
return `{| ${getNameFromProps(codec.props)} |}`
1633-
} else if (isPartialCodec(codec)) {
1693+
} else if (isPartialC(codec)) {
16341694
return getPartialTypeName(`{| ${getNameFromProps(codec.props)} |}`)
16351695
}
16361696
return `Exact<${codec.name}>`
@@ -2109,14 +2169,27 @@ function intersectTags(a: Tags, b: Tags): Tags {
21092169
return r
21102170
}
21112171

2172+
// tslint:disable-next-line: deprecation
2173+
function isAnyC(codec: Any): codec is AnyC {
2174+
return (codec as any)._tag === 'AnyType'
2175+
}
2176+
21122177
function isLiteralC(codec: Any): codec is LiteralC<LiteralValue> {
21132178
return (codec as any)._tag === 'LiteralType'
21142179
}
21152180

2181+
function isKeyofC(codec: Any): codec is KeyofC<Record<string, unknown>> {
2182+
return (codec as any)._tag === 'KeyofType'
2183+
}
2184+
21162185
function isTypeC(codec: Any): codec is TypeC<Props> {
21172186
return (codec as any)._tag === 'InterfaceType'
21182187
}
21192188

2189+
function isPartialC(codec: Any): codec is PartialC<Props> {
2190+
return (codec as any)._tag === 'PartialType'
2191+
}
2192+
21202193
// tslint:disable-next-line: deprecation
21212194
function isStrictC(codec: Any): codec is StrictC<Props> {
21222195
return (codec as any)._tag === 'StrictType'

0 commit comments

Comments
 (0)