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