Issue
I've written a Typescript function which, given a relationship type, a direction, and a nominal nodeId, returns a list of nodes on the opposite end of that relationship. Eg
("A_TO_B", "from", "nodeAId") -> NodeB[]
Is it possible to type this function such that Typescript correctly narrows/infers the return type, without passing explicit type parameters?
Curiously in my current implementation, the inference does work if the caller explicitly passes the first two generic type parameters:
export function getNeighborsByRelationship<
T extends ExtractRelationshipsByNodeId<D>["type"],
D extends Extract<keyof Relationship, "from" | "to"> = Extract<
keyof Relationship,
"from" | "to"
>,
R extends ExtractRelationshipByType<T> = ExtractRelationshipByType<T>,
N extends Node = ExtractNodeById<
R[Exclude<Extract<keyof R, "from" | "to">, D>]
>
>(
type: R["type"],
dir: D,
id: R[D]
): {
nodes: N[];
relationships: R[];
}
function test () {
const id: AppleId = "A"
// this does not infer the return type correctly... :(
const res = getNeighborsByRelationship('APPLE_TO_CLOCK_AND_DOGGIE', 'from', id)
// however, this does:
// const res = getNeighborsByRelationship<'APPLE_TO_CLOCK_AND_DOGGIE', 'from'>('APPLE_TO_CLOCK_AND_DOGGIE', 'from', id)
return res?.nodes[0].label
}
But I'm unable to make the inference work without explicitly passing the generic types.
My current work, reproducible and complete with sample Node and Relationship types, is included in the Playground link. The structure of Node and Relationship types is provided for convenience only, since they would not be easy to change in the codebase I'm working in.
Solution
There are a few limitations of TypeScript's type inference algorithm which are preventing this from working.
The first missing feature is described at microsoft/TypeScript#20126: TypeScript cannot infer a type T
from an indexed access type T[K]
. Your type
parameter is of type R["type"]
, but because of this limitation, TypeScript cannot infer R
from this.
The second missing feature is described at microsoft/TypeScript#7234: TypeScript cannot infer a type T
from a generic constraint on another type parameter, like U extends F<T>
. Your T
parameter is not mentioned anywhere inside the function parameters; it appears only as a constraint on R
, so T
cannot be inferred at all.
Until and unless these are addressed, you'll need to change your generic type parameters. The general approach I suggest is to cut down to as few generic type parameters as possible (if you are using default type arguments as shortcuts to using inline type computations, that might be acceptable, but for the first pass at this you should not use defaults for this purpose. Either compute your types inline, or write utility types for them), and have your function parameters be of types as directly related to these type parameters as possible. Like this:
declare function getNeighborsByRelationship<
T extends Relationship["type"],
D extends "from" | "to">(
type: T,
dir: D,
id: ExtractRelationshipByType<T>[D]
): {
nodes: (
ExtractNodeById<ExtractRelationshipByType<T>[
Exclude<Extract<keyof ExtractRelationshipByType<T>, "from" | "to">, D>
]>)[];
relationships: ExtractRelationshipByType<T>[];
}
Here we have only two generic type parameters; T
corresponds exactly to the type of type
, and D
corresponds exactly to the type of D
. That means when you call the function, T
and D
should be inferrable from the first two function arguments. And those two type parameters should be enough information to compute all the rest of the types you care about, such as the type of id
, and the element types of the nodes
and relationships
property of the return value.
If that's too cluttered you could make utility types like
type N<R extends Relationship, D extends "from" | "to"> =
ExtractNodeById<R[
Exclude<Extract<keyof R, "from" | "to">, D>
]>;
declare function getNeighborsByRelationship<
T extends Relationship["type"],
D extends "from" | "to">(
type: T,
dir: D,
id: ExtractRelationshipByType<T>[D]
): {
nodes: N<ExtractRelationshipByType<T>, D>[];
relationships: ExtractRelationshipByType<T>[];
}
or use default type arguments, but that's out of scope here.
Anyway, let's just make sure it behaves as expected:
function test() {
const id: AppleId = "A"
const res = getNeighborsByRelationship('APPLE_TO_CLOCK_AND_DOGGIE', 'from', id)
// ^? const res: {
// nodes: ExtractNodeById<"C" | "D">[];
// relationships: AppleToClockAndDoggie[];
// }
return res?.nodes[0].label
}
const v = test()
// ^? const v: "Clock" | "Doggie"
Looks good! Inference happened correctly and the types are as you expected.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.