theahura
theahura

Reputation: 433

Typescript: passing interface as parameter for a function that expects a JSON type

This is an extension of Typescript: interface that extends a JSON type

Given a JSON type like:

type JSONValue = 
 | string
 | number
 | boolean
 | null
 | JSONValue[]
 | {[key: string]: JSONValue}

I'd like to find a way to tell typescript that interfaces that match the JSONType should be auto-casted when passed into functions that accept the JSONType. For example:

interface Foo {
  name: 'FOO',
  fooProp: string
}

const bar = (foo: Foo) => { return foo }

const wrap = (fn: (...args: JSONValue[]) => JSONValue, args: JSONValue[]) => {
  return fn(...args);
}

wrap(bar, {name: 'FOO', fooProp: 'hello'});

This currently fails with:

Argument of type '(foo: Foo) => Foo' is not assignable to parameter of type '(...args: JSONValue[]) => JSONValue'.
  Types of parameters 'foo' and 'args' are incompatible.
    Type 'JSONValue' is not assignable to type 'Foo'.
      Type 'null' is not assignable to type 'Foo'.

even though analytically we know that the passed in foo is a JSON compatible type.

Playground here

Is there a way to get typescript to recognize that this interface is in fact also JSON?

Upvotes: 1

Views: 1038

Answers (1)

Tobias S.
Tobias S.

Reputation: 23885

There are multiple issues within your implementation currently. You are trying to type the function wrap as if it accepts a function fn which may have arbitrary JSONValue[] data passed into it. But then you are passing in bar, a function which only accepts Foo. To fix this, we can make wrap generic over T where T represents the parameters of fn. We would then type args to be of type T too, so that we can pass args to fn safely.

Notice how I made args a spread parameter which it was not before. We don't want args to be a tuple type, as you don't pass a tuple to the function when calling it.

const wrap = <T extends JSONValue[]>(
  fn: (...args: T) => JSONValue, 
  ...args: T
) => {
  return fn(...args);
}

But this will still not work. Foo is currently an interface which don't have implicit index signatures (see #15300). An interface can not fulfill the {[key: string]: JSONValue} constraint due to this missing implicit index signature. The only workaround we have without widening the JSONValue type is to convert Foo to be a type.

type 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'});

Playground

Upvotes: 3

Related Questions