React with TypeScript - 确保至少有一个类型的键不为空,即使它们都是可选的

问题描述 投票:0回答:2

我在 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。如何解决这个问题?

javascript reactjs typescript types ramda.js
2个回答
0
投票

您可以对类型进行一些更改以帮助解决此问题。

承认

EventRecord
中类型
string | null
的所有键都是相关的(并且是需要在运行时代码中检查以获得最佳匹配的键),您可以使用
const 在数组中定义它们
assertion
然后使用它来定义您的类型以及 optional
time
属性。然后你可以定义另一个具有
time
属性的类型作为非可选的。

可能看起来像这样:

TS游乐场

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()
一起使用按时间排序(最新的第一个):

TS游乐场

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,供您在此处的代码片段中运行:

TS游乐场

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 组件一起使用时,您应该没有任何问题:

TS游乐场

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 函数。


0
投票

第一步是修复

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的启发)。我相信你可以写出比这更好的东西,但把它作为一个起点(也看看这篇关于 SO 的帖子);

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
。如果只是一次那么你可以跳过上面的内容。

© www.soinside.com 2019 - 2024. All rights reserved.