You can use an existing solution such as typescript-is, although that may require you switch to ttypescript (a custom build of the compiler that allows plugins)
If you want a custom solution, we can build one in plain TS. First the requirements:
- Validate that a property is of a specific type
- Ensure that new fields are validated.
The last requirement can be satisfied by having an object with the same keys as A, with all keys required and the value being the type of the property. The type of such an object would be Record<keyof A, Types>. This object can then be used as the source for the validations, and we can take each key and validate it's specified type:
interface A {
a: string
}
type Types = "string" | "number" | "boolean";
function asA(data: unknown): A {
const keyValidators: Record<keyof A, Types> = {
a: "string"
}
if (typeof data === 'object' && data !== null) {
let maybeA = data as A
for (const key of Object.keys(keyValidators) as Array<keyof A>) {
if (typeof maybeA[key] !== keyValidators[key]) {
throw new Error('data is not an A');
}
}
return maybeA;
}
throw new Error('data is not an A');
}
let data: unknown = JSON.parse('{"a": "yes"}');
let a = asA(data);
Play
We could go further, and make a generic factory function that can validate for any object type and we can also allow some extra things, like specifying a function, or allowing optional properties:
interface A {
a: string
opt?: string
// b: number // error if you add b
}
function asOptional<T>(as: (s: unknown, errMsg?: string) => T) {
return function (s: unknown, errMsg?: string): T | undefined {
if (s === undefined) return s;
return as(s);
}
}
function asString(s: unknown, errMsg: string = ""): string {
if (typeof s === "string") return s as string
throw new Error(`${errMsg} '${s} is not a string`)
}
function asNumber(s: unknown, errMsg?: string): number {
if (typeof s === "number") return s as number;
throw new Error(`${errMsg} '${s} is not a string`)
}
type KeyValidators<T> = {
[P in keyof T]-?: (s: unknown, errMsg?: string) => T[P]
}
function asFactory<T extends object>(keyValidators:KeyValidators<T>) {
return function (data: unknown, errMsg: string = ""): T {
console.log(data);
if (typeof data === 'object' && data !== null) {
let maybeT = data as T
for (const key of Object.keys(keyValidators) as Array<keyof T>) {
keyValidators[key](maybeT[key], errMsg + key + ":");
}
return maybeT;
}
throw new Error(errMsg + 'data is not an A');
}
}
let data: unknown = JSON.parse('{"a": "yes"}');
const asA = asFactory<A>({
a: asString,
opt: asOptional(asString)
/// b: asNumber
})
let a = asA(data);
interface B {
a: A
}
const asB = asFactory<B>({
a: asA
})
let data2: unknown = JSON.parse('{ "a": {"a": "yes"} }');
let b = asB(data2);
let berr = asB(data);
Playground Link