Reputation: 6735
I am trying to use conditional type to link two parameters in a function, but I couldn't sort out to link 2 parameters when defining a function. Below is the simplified sample or the live version.
type Circle = { type: "circle"; data: { radius: number } };
type Rect = { type: "rect"; data: { width: number; height: number } };
type DefinedShape = Circle | Rect;
type FilterShapeByType<Shape, Type> = Shape extends { type: Type }
? Shape
: never;
type DrawShape = <T extends DefinedShape["type"]>(
type: T,
data: FilterShapeByType<DefinedShape, T>["data"]
) => void;
let drawShape1: DrawShape = () => {};
drawShape1("rect", { height: 12, width: 22 }); // pass
drawShape1("rect", { height: 12, width: 22, radius: 23 }); // failed, radius is not defined
drawShape1("circle", { radius: 12 }); // pass
drawShape1("circle", { radius: 12, len: 23 }); // failed, len is not defined
const drawShape2: DrawShape = (type, data) => {
switch (type) {
case "circle":
console.log(data.radius); // failed, Property 'radius' does not exist on type '(FilterShapeByType<Circle, T> | FilterShapeByType<Rect, T>)["data"]'.
break;
case "rect":
console.log(data.height, data.width);
break;
}
};
the original goal is to link the two parameters in DrawShape
function, meaning when type
is circle
, the data
should be {radius: number}
. I use the conditional type FilterShapeByType
to sort it out.
All good before I try to define a function drawShape2
. I am expecting when I use switch for the type
for circle
, the typescript should smart enough to assume data
should be the Circle['data']
, however it didn't. Wondering is there easy to make it automatically?
Upvotes: 3
Views: 123
Reputation: 10345
The Why
That's not how the force TypeScript works.
Let's take a closer look at what the type of data
is in the drawShape2
signature:
(parameter) data: (FilterShapeByType<Circle, T> | FilterShapeByType<Rect, T>)["data"]
What does it tell us? That the type of data
depends on what T
is resolved to. In your case, T
is constrained to DefinedShape["type"]
, or, inlined, "circle" | "rect"
(off-note: unless you want to confuse people, please, change the type
to something more customary like kind
).
Until you call the function with either "circle"
or "rect"
as type
, T
stays unresolved. With that said, let's test what is the actual type of data
given the union "circle" | "rect"
:
type testFilter = FilterShapeByType<DefinedShape, "circle"|"rect">["data"];
// type testFilter = {
// radius: number;
// } | {
// width: number;
// height: number;
// }
It should become obvious why you can't access either of the properties: because you can only access members without type guards if they are present on all constituents of the union (see discriminated unions). switch
statement does not matter here as the compiler is unable to narrow the union due to unresolved T
.
If you call the function, you will see there is no error:
drawShape2("circle", { radius:5 }) //OK, no error
The What
Now, what could be done to solve this?
There are several approaches, most are outlined in other answers, I would only like to mention that if you are in control of the public contract, use the DefinedShape
directly since information necessary for narrowing is contained in type
property:
const drawShape3 = (shape: DefinedShape) => {
switch(shape.type) {
case "circle": {
console.log(shape.data.radius); //OK
break;
}
case "rect": {
console.log(shape.data.height, shape.data.width); //OK
break
}
}
};
Upvotes: 1
Reputation: 6853
I'm curious of why your code isn't working. Looks like it should work, but it doesn't! I might have missed something, or it's a limitation from TypeScript.
Either way, here's a workaround that I prefer (using type guard):
type Shapes = {
circle: { radius: number },
rect: { width: number; height: number }
}
type ShapeTypes = keyof Shapes;
type DrawShape = <T extends ShapeTypes>(type: T, data: Shapes[T]) => void;
const isShapeOf = <T extends ShapeTypes>(expected: T, type: string, data: any): data is Shapes[T] => {
return expected === type;
}
const drawShape: DrawShape = (type, data) => {
if (isShapeOf("circle", type, data)) {
data.radius; // pass
} else if (isShapeOf("rect", type, data)) {
data.radius; // radius does not exist
data.width; // pass
data.height; // pass
}
};
drawShape("rect", { height: 12, width: 22 }); // pass
drawShape("rect", { height: 12, width: 22, radius: 23 }); // failed, radius is not defined
drawShape("circle", { radius: 12 }); // pass
drawShape("circle", { radius: 12, len: 23 }); // failed, len is not defined
Upvotes: 2
Reputation: 33051
There is one trick. Don't tell anybody :D
Just use named tuples
function drawShape2(...args: [type: 'circle', data: Circle['data']] | [type: 'rect', data: Rect['data']]) {
switch (args[0]) {
case "circle":
const [type1, data1] = args;
break;
case "rect":
const [type2, data2] = args
break;
}
};
Upvotes: 2