我在 Typescript 中有一个
event
类型,看起来像这样:
export type EventRecord = {
name: string;
eta: string | null;
assumed_time: string | null;
indicated_time: string | null;
};
和显示该事件时间的函数:
export const displayTime = (event: EventRecord): string | null =>
event.indicated_time || event.assumed_time || event.eta;
我想要实现的是确保三个时间(eta、assumed_time、indicated_time)中至少有一个不为空,这样当我的事件被标记为完成时,我就可以在时间轴上显示那个时间成分。我先用
Remeda
过滤我的事件,过滤事件只标记事件,然后显示它们:
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>
);
})}
这是我的 eventMarked 函数,它基本上检查是否至少提供了一次:
export const eventMarked = (event: eventRecord): boolean =>
!!(event.assumed_time || event.indicated_time || event.eta);
此设置的问题是我不断收到错误:
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) => {
| ^^^
我认为这个错误的原因可能是 typescript 并不知道 EventRecord 是什么类型,因为 display 要么是 null 要么是 string。如何解决这个问题?
您可以对类型进行一些更改以帮助解决此问题。
承认
EventRecord
中类型 string | null
的所有键都是相关的(并且是需要在运行时代码中检查以获得最佳匹配的键),您可以使用 const
在数组中定义它们
assertion 然后使用它来定义您的类型以及 optional time
属性。然后你可以定义另一个具有 time
属性的类型作为非可选的。
可能看起来像这样:
const eventRecordTimeKeys = ["indicated_time", "assumed_time", "eta"] as const;
type TimeKey = typeof eventRecordTimeKeys[number];
type EventRecord = Record<TimeKey, string | null> & {
name: string;
time?: Date;
};
/* EventRecord looks like this when expanded: {
assumed_time: string | null;
eta: string | null;
indicated_time: string | null;
name: string;
time?: Date;
} */
type EventRecordWithTime = EventRecord & { time: Date };
然后,您可以将现有的过滤器函数转换为 type guard,它以谓词作为其返回类型。这将帮助编译器理解,如果此函数返回
true
,则对象元素肯定具有时间属性,即 Date
。在下面代码中共享的版本中,我编写了函数来首先检查预期的属性是否存在以及类型是否正确。如果不存在,则使用上面的数组检查每个时间键,如果存在,则设置 time
属性,函数提前返回 true
。如果都是假的,那么函数返回false
.
我还包括一个标准的比较功能与
Array.prototype.sort()
一起使用按时间排序(最新的第一个):
function hasTime(event: EventRecord): event is EventRecordWithTime {
if (event.time instanceof Date) return true;
for (const key of eventRecordTimeKeys) {
const value = event[key];
if (value) {
event.time = new Date(value);
return true;
}
}
return false;
}
function sortByLatestTime(a: { time: Date }, b: { time: Date }): number {
return b.time.getTime() - a.time.getTime();
}
将其放在一个可重现的示例中看起来像这样,我在下面包含了已编译的 JS,供您在此处的代码片段中运行:
const events: EventRecord[] = [
{ name: "a", assumed_time: null, eta: "2000-01-01T00:00:00.000Z", indicated_time: null },
{ name: "b", assumed_time: null, eta: null, indicated_time: null },
{ name: "c", assumed_time: null, eta: null, indicated_time: "2001-01-01T00:00:00.000Z" },
{ name: "d", assumed_time: null, eta: null, indicated_time: "2002-01-01T00:00:00.000Z" },
{ name: "e", assumed_time: "2003-01-01T00:00:00.000Z", eta: null, indicated_time: null },
];
const timelineEvents = events
.filter(hasTime)
.sort(sortByLatestTime);
timelineEvents.forEach(({ name, time }) => console.log(`${name}: ${time}`)); // Order: e d c a
编译后的 JS 片段:
"use strict";
const eventRecordTimeKeys = ["indicated_time", "assumed_time", "eta"];
function hasTime(event) {
if (event.time instanceof Date)
return true;
for (const key of eventRecordTimeKeys) {
const value = event[key];
if (value) {
event.time = new Date(value);
return true;
}
}
return false;
}
function sortByLatestTime(a, b) {
return b.time.getTime() - a.time.getTime();
}
const events = [
{ name: "a", assumed_time: null, eta: "2000-01-01T00:00:00.000Z", indicated_time: null },
{ name: "b", assumed_time: null, eta: null, indicated_time: null },
{ name: "c", assumed_time: null, eta: null, indicated_time: "2001-01-01T00:00:00.000Z" },
{ name: "d", assumed_time: null, eta: null, indicated_time: "2002-01-01T00:00:00.000Z" },
{ name: "e", assumed_time: "2003-01-01T00:00:00.000Z", eta: null, indicated_time: null },
];
const timelineEvents = events
.filter(hasTime)
.sort(sortByLatestTime);
timelineEvents.forEach(({ name, time }) => console.log(`${name}: ${time}`));
与 React 组件一起使用时,您应该没有任何问题:
declare function TimelineEntry (props: { name: string; time: Date; }): ReactElement;
function Component() {
return timelineEvents.map(
({ name, time }) => <TimelineEntry key={name} name={name} time={time} />
);
}
就 Ramda 函数库而言:您没有说明问题中的
events
是什么,所以我无法重现您的示例。 (你确定你正确使用了 pipe
吗?)无论如何,Ramda 并不是这个上下文所必需的,但是如果需要,你可以将上面的 TS 技术应用到 Ramda 函数。
第一步是修复
displayTime
:您正在使用的 sortBy()
重载接受 (x: T) => Comparable
其中 Comparable
是 string | number | boolean
和 displayTime
返回 string | null
.
如果确定定义了其中一个时间属性,则将
displayTime
更改为:
const displayTime = (event: EventRecord): string =>
event.indicated_time || event.assumed_time || event.eta!;
请注意,如果它们不能是空字符串(只能是
null
或有效值),那么您还应该将||
更改为??
。
如果这些属性不能全部
null
那么我们应该在类型定义中表达这个约束。
为了解决这个问题,我们可能需要更改类型以反映我们想要强制执行的内容:至少其中一个属性不能为空。为此,我们首先需要引入一个小型帮助器(很大程度上受到RequireAtLeastOne
type RequireAtLeastOneOf<T> = {
[K in keyof T]: { [P in K]: NonNullable<T[P]> } & { [P in Exclude<keyof T, K>]: T[P] }
} [keyof T];
(我不喜欢你在
!
中仍然需要displayTime
的事实,但我认为应该可以帮助TS从string
推断event.indicated_time ?? event.assumed_time ?? event.eta
。)
为清楚起见,您可以将这些时间属性分开:
type EventTime = {
eta: string | null;
assumed_time: string | null;
indicated_time: string | null;
};
type EventRecordWithTime = RequireAtLeastOneOf<EventTime> & {
name: string;
};
就是这样:
const valid: EventRecordWithTime = {
name: "1",
assumed_time: "2000/01/01",
indicated_time: null,
eta: null
};
// Error: indicated_time and eta are required
const notValid1: EventRecordWithTime = {
name: "1",
assumed_time: "2000/01/01",
};
// Error: all required properties are missing
const notValid2: EventRecordWithTime = {
name: "1",
};
// Error: at least one of the "time" properties must not be null
const notValid3: EventRecordWithTime = {
name: "1",
assumed_time: null,
indicated_time: null,
eta: null
};
如果你经常使用它,那么你可以将
eventMarked()
更改为 type guard 并返回 event is EventRecordWithTime
。如果只是一次那么你可以跳过上面的内容。