I solved this using the ts-interface-builder and ts-interface-checker libraries and have been happy with the result. You write a plain TS type (rather than a custom syntax like io-ts has) and then run a codegen step to make a runtime representation of the type. Then you can create a checker from that and do runtime type checking.