In an attempt to tie the types to the arguments passed, consider the following example:
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 issue is that f(x)
has type number | string
instead of Data<T>
. It appears to stem from f
being of type Funcs[T]
rather than Func<T>
, and x
having type Obj[T]
rather than Data<T>
.
TypeScript struggles to infer that Funcs[T]
and Func<T>
are equivalent, and Obj[T]
and Data<T>
are as well, despite their definitions within Funcs
and Obj
respectively...
To resolve this issue, one can resort to typecasting everything, though it goes against the purpose of utilizing 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>
// Notice the double-casting as Obj[T] and Data<T> "do not sufficiently overlap."
return f(x)
}
Is there a way to achieve the desired result in TypeScript?
EDIT:
While the approach below may solve issues at call sites, it defeats the purpose of code reusability - combining logic on different types was the original aim, which is lost through repetition.
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 solution to avoid this redundancy while maintaining type safety?