Skip to content

Instantly share code, notes, and snippets.

@dandelany
Created May 21, 2019 00:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dandelany/a836566569dfeff09784e3fbeacbfde1 to your computer and use it in GitHub Desktop.
Save dandelany/a836566569dfeff09784e3fbeacbfde1 to your computer and use it in GitHub Desktop.
io-ts exactStrict codec
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