【问题标题】:How to map objects in a discriminated union to functions they can be called with?如何将可区分联合中的对象映射到可以调用它们的函数?
【发布时间】:2021-12-19 19:40:53
【问题描述】:

在 vanilla JS 中,我可以编写如下代码:

function renderTextField(props) { }
function renderSelectField(props) { }

const fieldMapping = {
    text: renderTextField,
    select: renderSelectField,
};

function renderField(field) {
    const renderFn = fieldMapping[field.type];
    renderFn(field.data);
}

使用 2 种类型的字段只是为了保持示例的小,但是这段代码的好处是通用方法不需要知道字段的类型,它将决定委托给 @ 提供的映射987654323@.

我正在尝试在 TypeScript 中编写类似的东西。但我不知道如何让这些类型工作并仍然使用一个对象来提供type 和委托给的函数之间的映射。

我意识到我可以使用 switch 语句或条件而不是对象来映射事物,但如果可能的话,我更愿意这样做。

type TextFieldData = { value: string }
type TextField = { type: 'text', data: TextFieldData }
type SelectFieldData = { options: string[], selectedValue: string }
type SelectField = { type: 'select', data: SelectFieldData }
type FormField = TextField | SelectField

function renderTextField(props: TextFieldData) {}
function renderSelectField(props: SelectFieldData) {}

const fieldMapping = {
  text: renderTextField,
  select: renderSelectField,
}

// This won't work!
function renderFieldDoesNotWork(field: FormField) {
  const renderFn = fieldMapping[field.type]

  // Type 'TextFieldData' is missing the following properties from type 'SelectFieldData': options, selectedValue
  renderFn(field.data)
}

// This works
function renderFieldWorks(field: FormField) {
  if (field.type === 'text') {
    const renderFn = fieldMapping[field.type]
    renderFn(field.data)
  } else if (field.type === 'select') {
    const renderFn = fieldMapping[field.type]
    renderFn(field.data)
  }
}

【问题讨论】:

  • 看起来像另一个问题要添加到一堆... TypeScript 缺乏对correlated record types 的良好支持,因此您可能需要使用类型断言。

标签: typescript


【解决方案1】:

恐怕您将不得不使用type assertion 来避免这里的代码重复。 TypeScript 的类型系统不能很好地支持这些 "correlated record types" 或任何依赖于两个联合类型值的交互的操作,其中联合不独立。

您已经找到了冗余代码在switch-statement 的解决方法;这是不安全断言的解决方法:

function assertNarrowFunction<F extends (arg: any) => any>(f: F) {
  return f as (arg: Parameters<F>[0]) => ReturnType<F>; // assert
}

这需要一个联合类型的函数,如((a: string)=&gt;number) | ((a: number)=&gt;boolean),并且不安全地将其缩小到一个函数,该函数接受其参数类型的联合并返回其返回类型的联合,如((a: string | number) =&gt; string | number) .这是不安全的,因为前联合类型的函数可能类似于const f = Math.random()&lt;0.5 ? ((a: string)=&gt;a.length) : ((a: number)=&gt;number.toFixed()),它绝对 匹配((a: string | number) =&gt; string | number)。我不能安全地调用f(5),因为f 可能是字符串长度函数。

无论如何,您可以在renderFn 上使用这种不安全的缩小范围来消除错误:

function renderFnAssertion(field: FormField) {
  const renderFn = assertNarrowFunction(fieldMapping[field.type]);
  renderFn(field.data); // okay
}

您在renderFn 的类型上向编译器撒了一点谎... 与其说它会接受任何旧参数(例如,renderFn(123) 将根据需要失败),但足以让它允许这个:

function badRenderFn(field1: FormField, field2: FormField) {
  const renderFn1 = assertNarrowFunction(fieldMapping[field1.type]);
  renderFn1(field2.data); // no error!!! ooops
}

所以你必须小心。

好的,希望对您有所帮助;祝你好运!

Link to code

【讨论】:

  • 感谢您的精彩解释。我希望 TS 对此有更好的支持!
【解决方案2】:

可能的方法

这里是in the playground

这种方法的一个好处是,如果我们删除所有类型信息,我们会从您的问题中留下原始的原生 JavaScript。我看到的唯一缺点是as 解决了jcalz 指出的缺乏相关记录类型的问题。在这种情况下看起来很好,因为我们知道的比编译器知道的要多,而且我们并没有失去任何类型安全性。

type TextFieldData = { value: string }
type TextField = { type: 'text', data: TextFieldData }
type SelectFieldData = { options: string[], selectedValue: string }
type SelectField = { type: 'select', data: SelectFieldData }
type FormField = TextField | SelectField

function renderTextField(props: TextFieldData) { }
function renderSelectField(props: SelectFieldData) { }

const fieldMapping = {
    text: renderTextField,
    select: renderSelectField,
}

// This is the new block of code.
type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;
type TParam<Type> = FindByType<FormField, Type>['data'];
type TFunction<Type> = (props: TParam<Type>) => void;

function renderFieldDoesNotWork(field: FormField) {
    // This is the cast that seems unavoidable without correlated record types.
    const renderFn = fieldMapping[field.type] as TFunction<typeof field.type>;
    renderFn(field.data)
}

附加类型安全

在您问题的代码中,如果开发人员在映射中犯了这样的错误,编译器不会抱怨:

const fieldMappingOops = {
    select: renderTextField, // no compiler error
    text: renderTextField,
}

我们可以添加一个新类型来告诉编译器在这种情况下抱怨:

type FieldMapping = {
    [Key in FormField['type']]: TFunction<Key>;
}

const fieldMappingOops: FieldMapping = {
    select: renderTextField, // compiler error
    text: renderTextField,
}

其他详情

This GitHub comment by Ryan Cavanaugh 启发了这种方法。当我们将 switchif 语句与 tagged union / discriminated union 类型一起使用时,FindByType 为我们提供了一些类型缩小能力。

以下是在给定相关输入时与该评论相关的类型如何扩展:

// type f1 = {
//     type: "text";
//     data: TextFieldData;
// }
type f1 = FindByType<FormField, 'text'>;

// type f2 = {
//     value: string;
// }
type f2 = TParam<'text'>;

// type f3 = (props: TextFieldData) => void
type f3 = TFunction<'text'>;

// type f4 = TextField | SelectField
type f4 = FindByType<FormField, 'text' | 'select'>;

// type f5 = TextFieldData | SelectFieldData
type f5 = TParam<'text' | 'select'>;

// type f6 = (props: TextFieldData | SelectFieldData) => void
type f6 = TFunction<'text' | 'select'>;

【讨论】:

  • 谢谢——这里有一些有用的花絮,我喜欢FindByType
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-02-27
  • 2021-07-10
  • 1970-01-01
  • 2019-04-20
  • 2013-02-11
  • 1970-01-01
  • 2017-02-08
相关资源
最近更新 更多