I am struggling to automatically infer the type of different kind of items based on their geometry (in the context of displaying some GeoJSON data).
I am using a generic types, therefore I did not manage to set a custom typeguards, since it would allow me to distinguish "Individual" items from "Aggregates", but not different type of "Individual" items.
Basically, I need to level of inference:
I've created a simplified example, in my real app I have 4 different types of items which may have different possible geometries.
Here is a TypeScript playground, and the code below:
type A = {type: "A", a: string}
type B = {type: "B", b: string}
type C = {type: "C", c: string}
type Geometries = A | B | C
type IndividualFeature<G extends A | B = A | B> = { geometry: G, indivAttribute: string}
type AggregateFeature = { geometry: C, aggAttribute: string}
type DisplayableFeature = IndividualFeature | AggregateFeature
const display = (feature: DisplayableFeature) => {
switch(feature.geometry.type) {
case "A":
console.log("type A", feature.geometry.a, feature.indivAttribute);
return;
case "B":
console.log("type B", feature.geometry.b, feature.indivAttribute)
return;
case "C":
console.log("type C", feature.geometry.c, feature.aggAttribute)
default:
// should not happen
}
}
const indivFeature: IndividualFeature = { geometry: { type: "A", a: "a"}, indivAttribute: "hello indiv"}
const aggFeature: AggregateFeature = { geometry: { type: "C", c: "c"}, aggAttribute: "hello agg"}
The geometry is correctly discriminated, but not individually vs aggregates (the feature.indivAttribute
/feature.aggAttribute
trigger an error).
For the record, I've tried a typeguard: this allows me to differentiate "Indiv" and "Aggregates", but I've lost the discrimination of the geometry.
How should I structure my types/code so feature.indivAttribute
is correctly recognized as a valid attribute in this example?
The concept of discriminated unions is how TypeScript differentiates between those objects and does so in a way that scales extremely well, even with larger sets of objects. As such, we had to create a new ANIMAL_TYPE property on both types that holds a single literal value we can use to check against.
In F#, a sum type is called a “discriminated union” type. Each component type (called a union case) must be tagged with a label (called a case identifier or tag) so that they can be told apart (“discriminated”). The labels can be any identifier you like, but must start with an uppercase letter.
This is indeed a typescript limitation, even without the generic. There is an existing github issue here: microsoft/TypeScript#18758. There is also a PR with some recent activity: microsoft/TypeScript#38839.
Narrowing a union based on a nested discriminated union is currently not possible. The discriminant must be on the same "level".
As a workaround you could write a custom type guard like so:
type AllTypes = DisplayableFeature["geometry"]["type"] // "A" | "B" | "C"
type FeatueOfType<T extends AllTypes> = {
"A": IndividualFeature<A>,
"B": IndividualFeature<B>,
"C": AggregateFeature
}[T]
function isFeatueOfType<T extends AllTypes>(
feature: DisplayableFeature, type: T
): feature is FeatueOfType<T> {
return feature.geometry.type === type
}
FeatueOfType<T>
maps a geometry type to its feature type. E.g. FeatueOfType<"A">
would equal IndividualFeature<A>
. (This type could potentially be produced from DisplayableFeature
directly instead of writing it by hand, but that might get complicated.)
When you then call the typeguard like isFeatueOfType(feature, "A")
and the feature.geometry.type === type
check succeeds, we tell typescript that the type of feature
has to be FeatueOfType<"A">
, i.e. IndividualFeature<A>
.
(Note that when there is a bug in a type guard, like writing feature.geometry.type !== type
above, typescript is not able to catch that. So it's always advisable to properly test them.)
Usage:
const display = (feature: DisplayableFeature) => {
if (isFeatueOfType(feature, "A")) {
doSometingWithA(feature) // typechecks
console.log("type A", feature.geometry.a, feature.indivAttribute);
}
else if (isFeatueOfType(feature, "B")) {
console.log("type B", feature.geometry.b, feature.indivAttribute)
}
else if (isFeatueOfType(feature, "C")) {
console.log("type C", feature.geometry.c, feature.aggAttribute)
}
else {
throw Error()
}
}
function doSometingWithA(a: IndividualFeature<A>) {}
(Playground link)
maybe just cast feature ?
switch(feature.geometry.type) {
case "A": console.log("type A", feature.geometry.a, (feature as IndividualFeature).indivAttribute);
return;
case "B":
console.log("type B", feature.geometry.b, (feature as IndividualFeature).indivAttribute)
return;
case "C":
console.log("type C", feature.geometry.c, (feature as AggregateFeature).aggAttribute)
default:
// should not happen
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With