Generic Typeguard for Inner type

82 Views Asked by At

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?

2

There are 2 best solutions below

4
wonderflame On BEST ANSWER

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;
}

playground

0
neurosie On

I ran across this issue myself, and wanted to share how to do this if you've already validated that the object is a Feature, you just want to narrow the geometry type:

function isFeature<T extends Feature["geometry"]["type"]>(
  feature: Feature,
  type: T,
): feature is Feature<Extract<Geometry, { type: T }>> {
  return feature.geometry.type === type;
}

if (isFeature(foo, "Polygon")) {/* narrows foo to Feature<Polygon> */}

This works with @types/geojson.