Dependent types in typescript - determine type via the property name type

Viewed 1010

I was trying to make the types dependent on the type of the passed argument. A contrived example is:

type NS = "num" | "str"
type Data<T extends NS> = T extends "num" ? number : string
type Func<T extends NS> = (x: Data<T>) => Data<T>
type Funcs = {[T in NS]: Func<T>}
type Obj = {[T in NS]: Data<T>}
const funcs: Funcs = {
  num: (x) => x * 2,
  str: (x) => x + x
}

function useFunc<T extends NS>(ns: T, obj: Obj): Data<T> {
  const f = funcs[ns]
  const x = obj[ns]
  return f(x)
}

The problem is that f(x) has type number | string rather than Data<T>, that seems caused by f having type Funcs[T] rather than Func<T>. x also has type Obj[T] rather than Data<T>.

So typescript cannot infer that Funcs[T] and Func<T> are the same, and Obj[T] and Data<T> are the same, even though that is how Funcs and Obj are defined...

I can make it work by typecasting everything, but it defeats the purpose of using types:

function useFunc<T extends NS>(ns: T, obj: Obj): Data<T> {
  const f = funcs[ns] as Func<T>
  const x = obj[ns] as unknown as Data<T>
  // note the double-cast as Obj[T] and Data<T> "do not sufficiently overlap"
  return f(x)
}

Is there a way to make it work in typescript?

EDIT:

I know that it can be made work in this way, that works for the call sites, but pointless from code re-use point of view - combining the same logic on different types was the actual motivation to do it, and in this way it is simply repeated.

function useFunc<T extends NS>(ns: T, obj: Obj): Data<T>
function useFunc(ns: NS, obj: Obj): Data<NS> {
  if (ns === "num") {
    const f = funcs[ns]
    const x = obj[ns]
    return f(x)
  }

  const f = funcs[ns]
  const x = obj[ns]
  return f(x)
}

Is there a way to avoid this repetition without losing type safety?

1 Answers

The main problem is that you trying to call f function inside useFunc when TS infer f signature as (x: never) => string | numer. This signature is not compatible with every signature inside funcs object.

Also, as I said signature useFunc<T extends NS>(ns: T, obj: Obj): Data<T> should be simplified, or even it's better to say corrected, to useFunc<T extends NS>(ns: T, obj: Data<T>): Data<T>. That's because TS should know the type of data you will pass to f to be able to make type checking. Also, another reason requires the above modification is that useFunc function call available only with ns equal to "num" or "str", but not simultaneously. Because of the above, obj parameter type will be Data<"num"> or Data<"str">. Consequently, it's wrong to type it as Obj.

Possible solution with minimal changes will be:

function useFunc<T extends NS>(ns: T, arg: Data<T>): Data<T> {
  const f = funcs[ns]
  return (f as Func<T>)(arg)
}
Related