This is an extension of Typescript: passing interface as parameter for a function that expects a JSON type (asking about passing interfaces to JSON typed functions), which in turn is an extension of Typescript: interface that extends a JSON type (asking about casting to/from JSON types)
These questions relate to a JSON Typescript type:
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| {[key: string]: JSONValue}
In Typescript: passing interface as parameter for a function that expects a JSON type, the final answer indicates that it is not possible to pass an interface to a function that expects a JSON value. In particular, the following code:
interface Foo {
name: 'FOO',
fooProp: string
}
const bar = (foo: Foo) => { return foo }
const wrap = <T extends JSONValue[]>(
fn: (...args: T) => JSONValue,
...args: T
) => {
return fn(...args);
}
wrap(bar, { name: 'FOO', fooProp: 'hello'});
fails because the interface Foo
cannot be assigned to JSONValue
even though analytically it is easy to recognize that the cast should be fine.
see playground, as well as https://github.com/microsoft/TypeScript/issues/15300
The previous answer stated:
The only workaround we have without widening the JSONValue type is to convert [interface] Foo to be a type.
In my case, I can modify the JSONValue type but cannot easily modify all of the relevant interfaces. What would widening the JSONValue type entail?
What I initially meant in my answer was to loosen the type JSONValue
. You could settle for the object
type.
const wrap = <T extends object[]>(
fn: (...args: T) => object,
...args: T
) => {
return fn(...args);
}
But you are essentially losing type safety as the function now accepts types which should be invalid like
interface Foo {
name: 'FOO',
fooProp: string,
fn: () => void
}
which has a property fn
with a function type. Ideally we would not allow this type to be passed to the function.
But not all hope is lost. We have one option left: infer the types into a generic type and recursively validate it.
type ValidateJSON<T> = {
[K in keyof T]: T[K] extends JSONValue
? T[K]
: T[K] extends Function // we will blacklist the function type
? never
: T[K] extends object
? ValidateJSON<T[K]>
: never // everything that is not an object type or part of JSONValue will resolve to never
} extends infer U ? { [K in keyof U]: U[K] } : never
ValidateJSON
takes some type T
and traverses through its type. It checks the property of the type and resolves them to never
if the type should not be valid.
interface Foo {
name: 'FOO',
fooProp: string,
fn: () => void
}
type Validated = ValidateJSON<Foo>
// {
// name: 'FOO';
// fooProp: string;
// fn: never;
// }
We can use this utility type to validate both the parameter type and the return type of fn
inside of wrap
.
const wrap = <T extends any[], R extends ValidateJSON<R>>(
fn: (...args: T) => R,
...args: { [K in keyof T]: ValidateJSON<T[K]> }
) => {
return fn(...args as any);
}
Which all leads to the following behaviour:
// ok
wrap(
(foo: Foo) => { return foo },
{ name: 'FOO', fooProp: 'hello' }
);
// not ok, foo has a parameter type which includes a function
wrap(
(foo: Foo & { fn: () => void }) => { return foo },
{ name: 'FOO', fooProp: 'hello', fn: () => {} }
);
// not ok, fn returns an object which includes a function
wrap(
(foo: Foo) => { return { ...foo, fn: () => {} } },
{ name: 'FOO', fooProp: 'hello' }
);
// not ok, foo has a parameter type which includes undefined
wrap(
(foo: Foo & { c: undefined }) => { return foo },
{ name: 'FOO', fooProp: 'hello', c: undefined }
);