so i've got some issues, at work we use these types:
export type GeoJSON = Geometry | Feature;
export type GeoJsonTypes = GeoJSON['type'];
export interface GeoJsonObject { type: GeoJsonTypes; }
export interface Point extends GeoJsonObject {
type: 'Point';
coordinates: number[];
}
export interface MultiPoint extends GeoJsonObject {
type: "MultiPoint",
coordinates: number[][];
}
export type Geometry =
| Point
| MultiPoint
export interface Feature<G extends Geometry = Geometry>
extends GeoJsonObject {
type: 'Feature';
geometry: G;
}
The issue that we're having is that we end up having to create a Typeguard for each Feature at the callsites when we have Feature<Geometry>
that we want to narrow.
export function isPolygon(f: Feature): f is Feature<Polygon> {
return f && f.geometry && f.geometry.type === 'Polygon';
}
I want to make a generic typeguard, i used Generic Typeguard as base for my thinking but i'm stuck.
I know that i can use the Extract<Geometry, {type: U}>
to get my type such as this example.
export function isFeature<T extends Feature<Geometry>, U extends T['geometry']['type']>(
feature: T,
type: U
): feature is Feature<Extract<Geometry, { type: U }>> {
return feature && feature.geometry && feature.geometry.type === type;
}
The problem that i'm having is that if i constrain T to be of type Feature, i have no use for the T variable in the expression, it gives me an error saying A type Predicate's type must be assignable to it's parameter type
, If i constrain it to Geometry, then i can't use Feature<Extract<T, {type: U}>>
Cause T could be instantiated with an arbitrary type which could be unrelated
, is there any way to have the generic typeguard for a feature?
The issue is that in the type guard you type feature
as T
and you are checking whether feature
is something else other than T
, which is logically impossible to convert T
to Feature<Extract<Geometry, { type: U }>>
. Instead, you should only accept U
and keep the feature
nongeneric, just unknown
.
isFeature<U extends Feature<Geometry>["geometry"]["type"]>(
feature: unknown,
type: U
): feature is Feature<Extract<Geometry, { type: U }>> {}
This will cause many repetitive checks and to avoid them we can create a generic isObject
type guard that would turn any object type T
to Partial<Record<keyof T, unknown>>
. The reason to turn properties to unknown
and make the whole object partial is to make it as type-safe as possible:
const isObject = <T>(
arg: unknown
): arg is Partial<Record<keyof T, unknown>> => {
return !!arg && typeof arg === "object" && !Array.isArray(arg);
};
Next, we can extract a type guard for generic features, ideally you would also include the geometry.coordinates
into checking:
const isFeatureBase = (arg: unknown): arg is Feature => {
return (
isObject<Feature>(arg) &&
isObject<Feature["geometry"]>(arg.geometry) &&
typeof arg.type === "string" &&
typeof arg.geometry.type === "string"
);
};
Finally, our isFeature
will look like this:
function isFeature<U extends Feature<Geometry>["geometry"]["type"]>(
feature: unknown,
type: U
): feature is Feature<Extract<Geometry, { type: U }>> {
return isFeatureBase(feature) && feature.geometry.type === type;
}
Usage:
const a = {};
if (isFeature(a, "MultiPoint")) {
// "MultiPoint"
a.geometry.type;
}