I have an event
type in Typescript that looks like this:
export type EventRecord = {
name: string;
eta: string | null;
assumed_time: string | null;
indicated_time: string | null;
};
and function that displays the time of that event:
export const displayTime = (event: EventRecord): string | null =>
event.indicated_time || event.assumed_time || event.eta;
What I want to achieve is to make sure that at least one of the three times (eta, assumed_time, indicated_time) is not null, so that when my event is Marked as done I’ll be able to display that time on the timeline component. I filter my events with Remeda
to first, filter events tot only marked ones, and then displaying them:
const timelineEvents = R.pipe(
events,
R.filter(eventMarked),
R.sortBy(displayTime),
R.reverse
);
{timelineEvents.map((event: EventRecord) => {
const time = new Date(displayTime(event)!);
return (
<TimelineEntry
name={event.name}
time={time}
/>
</RecordContextProvider>
);
})}
Here is my eventMarked function, it basically check if there is at least one time provided:
export const eventMarked = (event: eventRecord): boolean =>
!!(event.assumed_time || event.indicated_time || event.eta);
The problem with this setup is that I keep getting errors:
S2345: Argument of type 'unknown[]' is not assignable to parameter of type '(input: EventRecord[]) => unknown'.
Type 'unknown[]' provides no match for the signature '(input: EventRecord[]): unknown'.
80 | events,
81 | R.filter(eventMarked),
> 82 | R.sortBy(displayTime),
| ^^^^^^^^^^^^^^^^^^^^^
83 | R.reverse
84 | );
TS2769: No overload matches this call.
Overload 1 of 2, '(array: readonly unknown[], ...sorts: SortRule<unknown>[]): unknown[]', gave the following error.
Argument of type '(event: EventRecord) => string | null' is not assignable to parameter of type 'readonly unknown[]'.
Overload 2 of 2, '(sort: SortRule<EventRecord>, ...sorts: SortRule<EventRecord>[]): (array: readonly EventRecord[]) => EventRecord[]', gave the following error.
Argument of type '(event: EventRecord) => string | null' is not assignable to parameter of type 'SortRule<EventRecord>'.
Type '(event: EventRecord) => string | null' is not assignable to type 'SortProjection<EventRecord>'.
Type 'string | null' is not assignable to type 'Comparable'.
Type 'null' is not assignable to type 'Comparable'.
80 | events,
81 | R.filter(eventMarked),
> 82 | R.sortBy(displayTime),
| ^^^^^^^^^^^
83 | R.reverse
84 | );
85 |
TS2339: Property 'map' does not exist on type '(array: readonly unknown[]) => unknown[]'.
114 | />
115 | )}
> 116 | {timelineEvents.map((event: EventRecord) => {
| ^^^
I think the cause of this error might be that typescript doesn’t really know what type is EventRecord because display is either null or string. How can this problem be solved?
2
Answers
You can make a few changes in your types to help solve this.
Acknowledging that all of the keys in
EventRecord
of typestring | null
are related (and are the ones that need to be checked in the runtime code for a best match), you can define them in an array using aconst
assertion and then use that to define your type along with an optionaltime
property. Then you can define another type that has thetime
property as non-optional.That might look like this:
TS Playground
Then, you can convert your existing filter function to a type guard, which has a predicate as its return type. This will help the compiler understand that if this function returns
true
, then the object element definitely has a time property that’s aDate
. In the version shared in the code below, I’ve written the function to first check if the expected property exists and is of the correct type. If not, the array from above is used to check each time key and — if one exists, thetime
property is set and the function returnstrue
early. If all are falsy, then function returnsfalse
.I’ve also included a standard compare function to use with
Array.prototype.sort()
to sort by time (latest first):TS Playground
Putting that together in a reproducible example looks like this, and I’ve included the compiled JS below for you to run in a code snippet here:
TS Playground
Compiled JS snippet:
When used with a React component, you shouldn’t have any trouble:
TS Playground
As far as the Ramda functional library is concerned: you don’t show what
events
is in your question, so I can’t reproduce your example. (Are you sure you’re usingpipe
correctly?) At any rate, Ramda isn’t strictly needed for this context, but you can adapt the TS techniques above to the Ramda functions if desired.The first step is to fix
displayTime
: thesortBy()
overload you’re using accepts(x: T) => Comparable
whereComparable
isstring | number | boolean
anddisplayTime
returnsstring | null
.If one of those time properties is surely defined (it is because you filtered the array with
eventMarked()
) then changedisplayTime
to:Note that if they cannot be an empty string (only
null
or a valid value) then you should also change||
to??
.Optional: do you use these "marked" events often and you want to capture this constraint in the type definition?
To fix this problem we may want to change the type to reflect what we want to enforce: at least one of those properties must not be null. To do it we first need to introduce a small type helper (heavily inspired from RequireAtLeastOne<T>). I’m sure you can write something better than this but take it as a starting point (check out also this post here on SO);
(I do not like the fact that you will still need
!
indisplayTime
but I think it should be possible to help TS to inferstring
fromevent.indicated_time ?? event.assumed_time ?? event.eta
.)For clarity you could separate those time properties:
That’s all:
If you use it often enough then you could change
eventMarked()
to be a type guard and returnevent is EventRecordWithTime
. If it’s just for once then you can skip the above.