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?
2
Answers
If you use a separate generic for the return type (which also extends
JsonValue
, then you won’t have a type compatibility issue, and you can also infer a more narrow return type:On the
JsonValue
type that you showed: for the union member which hasstring
keys andJsonValue
values (the object type): it is more correct to includeundefined
in a union withJsonValue
, making the keys effectively optional… because there will never be a value at every key in an object, and the value that results from accessing a key that doesn’t exist on an object isundefined
at runtime:This is compatible with the serialization and deserialization algorithms of the
JSON
object in JavaScript because it neither serializes properties withundefined
as a value, nor does JSON supportundefined
as a type, soundefined
will never exist in a deserialized value.This type is both convenient (you can use dot notation property access for any property name) and type-safe (you must narrow each value to be
JsonValue
to safely use it as such).Full code in TS Playground
Update in response to your comment:
It is only possible if the interface extends (is constrained by) the object-like union member of
JsonValue
.The TS handbook section Differences Between Type Aliases and Interfaces begins with this information (the emphasis is mine):
What this means is that each type alias is finalized at the site where it’s defined… but an interface can always be extended (mutated), so the exact shape is not actually finalized until type-checking happens because other code (e.g. other code that consumes your code) can change it.
The only way to prevent this is to constrain which extensions are allowed for the interface in question:
An example using your original, unconstrained interface shows the compiler error:
However, if the interface is constrained to only allow compatibility with the object-like union member of
JsonValue
, then there are no potential type compatibility issues:Code in TS Playground
See also: utility types
in the TS handbook
What I initially meant in my answer was to loosen the type
JSONValue
. You could settle for theobject
type.But you are essentially losing type safety as the function now accepts types which should be invalid like
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.
ValidateJSON
takes some typeT
and traverses through its type. It checks the property of the type and resolves them tonever
if the type should not be valid.We can use this utility type to validate both the parameter type and the return type of
fn
inside ofwrap
.Which all leads to the following behaviour:
Playground