Created
May 21, 2019 00:14
-
-
Save dandelany/a836566569dfeff09784e3fbeacbfde1 to your computer and use it in GitHub Desktop.
io-ts exactStrict codec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as t from 'io-ts'; | |
// --- utils taken from io-ts internals --- | |
const getIsCodec = <T extends t.Any>(tag: string) => ( | |
codec: t.Any | |
): codec is T => (codec as any)._tag === tag; | |
const isInterfaceCodec = getIsCodec<t.InterfaceType<t.Props>>('InterfaceType'); | |
const isPartialCodec = getIsCodec<t.PartialType<t.Props>>('PartialType'); | |
const getPartialTypeName = (inner: string): string => { | |
return `Partial<${inner}>`; | |
}; | |
const getNameFromProps = (props: t.Props): string => | |
Object.keys(props) | |
.map(k => `${k}: ${props[k].name}`) | |
.join(', '); | |
const getExactTypeName = (codec: t.Any): string => { | |
if (isInterfaceCodec(codec)) { | |
return `{| ${getNameFromProps(codec.props)} |}`; | |
} else if (isPartialCodec(codec)) { | |
return getPartialTypeName(`{| ${getNameFromProps(codec.props)} |}`); | |
} | |
return `Exact<${codec.name}>`; | |
}; | |
const getProps = (codec: t.HasProps): t.Props => { | |
switch (codec._tag) { | |
case 'RefinementType': | |
case 'ReadonlyType': | |
return getProps(codec.type); | |
case 'InterfaceType': | |
case 'StrictType': | |
case 'PartialType': | |
return codec.props; | |
case 'IntersectionType': | |
return codec.types.reduce<t.Props>( | |
(props, type) => Object.assign(props, getProps(type)), | |
{} | |
); | |
} | |
}; | |
const hasOwnProperty = Object.prototype.hasOwnProperty; | |
const stripKeys = (o: any, props: t.Props): unknown => { | |
const keys = Object.getOwnPropertyNames(o); | |
let shouldStrip = false; | |
const r: any = {}; | |
for (let i = 0; i < keys.length; i++) { | |
const key = keys[i]; | |
if (!hasOwnProperty.call(props, key)) { | |
shouldStrip = true; | |
} else { | |
r[key] = o[key]; | |
} | |
} | |
return shouldStrip ? r : o; | |
}; | |
// --- end utils taken from io-ts internals --- | |
// --- custom utils & `exactStrict` codec --- | |
const hasAdditionalKeys = (o: any, props: t.Props): boolean => { | |
// return true if `o` contains keys that aren't in `props` | |
const keys = Object.getOwnPropertyNames(o); | |
for (let i = 0; i < keys.length; i++) { | |
if (!hasOwnProperty.call(props, keys[i])) { | |
return true; | |
} | |
} | |
return false; | |
}; | |
export const exactStrict = <C extends t.HasProps>( | |
codec: C, | |
name: string = getExactTypeName(codec) | |
): t.ExactC<C> => { | |
const props: t.Props = getProps(codec); | |
return new t.ExactType( | |
name, | |
codec.is, | |
(u, c) => { | |
const unknownRecordValidation = t.UnknownRecord.validate(u, c); | |
if (unknownRecordValidation.isLeft()) { | |
return unknownRecordValidation; | |
} | |
const validation = codec.validate(u, c); | |
if (validation.isLeft()) { | |
return validation; | |
} | |
const additionalKeys = hasAdditionalKeys(validation.value, props); | |
return additionalKeys ? | |
t.failure(u, c, "Additional properties are not allowed") : | |
validation; | |
}, | |
a => codec.encode(stripKeys(a, props)), | |
codec | |
); | |
}; | |
// --- end custom utils & `exactStrict` codec | |
const User = exactStrict(t.type({ id: t.number, name: t.string })); | |
const goodTest = User.decode({ id: 1, name: 'dan'}); | |
console.log("good value validates: ", goodTest.isRight()); // returns true | |
const badTest = User.decode({ id: 2, name: "stan", bad: "news bears" }); | |
console.log("bad value validates: ", badTest.isRight()); // returns false |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment