Issue
I'd like to write a function asA that takes a parameter of type unknown and returns it as a specific interface type A, or throws an error if the parameter doesn't match the interface type A.
The solution is supposed to be robust. I.e. if add a new field to my interface type A, the compiler should complain about my function missing a check for the new field until I fix it.
Below is an example of such a function asA, but it doesn't work. The compiler says:
Element implicitly has an 'any' type because expression of type '"a"' can't be used to index type '{}'. Property 'a' does not exist on type '{}'.(7053)
interface A {
a: string
}
function asA(data:unknown): A {
if (typeof data === 'object' && data !== null) {
if ('a' in data && typeof data['a'] === 'string') {
return data;
}
}
throw new Error('data is not an A');
}
let data:unknown = JSON.parse('{"a": "yes"}');
let a = asA(data);
How can I write a function asA as outlined above?
I'm fine with using typecasts, e.g. (data as any)['a'], as long as there are no silent failures when new fields are added to A.
Solution
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);
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);
Answered By - Titian Cernicova-Dragomir
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.