Home > database >  Is it possible to create a type guard to check whether an object field is undefined?
Is it possible to create a type guard to check whether an object field is undefined?

Time:09-05

I'm trying to come up with typings for a code I write. Suppose I have following interfaces:

interface Car {
    id: string;
    type: number
    ownerId: number | undefined;
    // ... other fields
}

interface Plane {
    id: number;
    ownerId: number | undefined;
    // ... other fields
}

I will often use them with Array methods like so:

const fCars = cars
    .filter(car => car.ownerId !== undefined)
    .filter(car => car.type === 1)

const fPlanes = planes
    .filter(plane => plane.ownerId === 1)

// fCars and fPlanes are Car[] and Planes[] respectively

Because a lot of predicates are either similar or pretty much the same, I wanted to export them as functions to achieve result like following code:

const filtered = cars
    .filter(byDefined('ownerId'))
    .filter(byEq('type', 1))

const filtered = planes
    .filter(byEq('ownerId', 1))

// for some fields, I would like to shorten this even further to just

const filtered = planes
    .filter(byOwnerIdEq(1))

I've managed to come up with composeEqual function that compares any field to a value of known type:

export const composeEqual =
    <
        Key extends string,
        Value extends unknown,
        CheckType extends { [key in Key]: Value },
        Type extends CheckType
    >(
        key: Key,
        value: Value
    ) =>
    (obj: Type): obj is Type & CheckType =>
        obj[key] === value;

export const byOwnerIdEq = (ownerId: string) => composeEqual('ownerId', ownerId);

However this still doesn't solve my problem fully - when a field's type is a union, TS report an error. Going back to my previous example

const filtered = cars
    .filter(byDefined('ownerId'))
    .filter(byEq('type', 1)) // error

I would like byDefined function to type guard cars from ownerId possibly being undefined.

The new type should be:

type N = Omit<Car, 'ownerId'> & { ownerId: number }

Is this possible to achieve this functionality with TS?

CodePudding user response:

You were close! Here is a solution using conditional types. Note that you can use Exclude instead of NonNullable if you want to allow null as well.

function byDefined<
    Type,
    Key extends keyof Type,
    DefinedType extends {[Property in keyof Type]: Property extends Key ? NonNullable<Type[Property]> : Type[Property]}
>(key: Key) {
    return (obj: Type): obj is DefinedType => {
        return obj[key] !== undefined;
    };
}

/**********************/

interface Car {
    id: string;
    type: number
    ownerId: number | undefined;
    // ... other fields
}

const cars: Car[] = [];
cars[0].ownerId; // number | undefined

const definedCars = cars.filter(byDefined('ownerId'));
definedCars[0].ownerId; // number

TS Playground

  • Related