TypeScript:如何将已区分联合中的对象映射到可以使用它们调用的函数?

时间:2019-06-26 21:17:46

标签: typescript

在香草JS中,我能够编写一些类似于以下内容的代码:

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

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

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

仅使用两种类型的字段,以使示例保持较小,但是此代码的好处是,通用方法不需要了解字段的类型,并且将决策委托给{ {1}}。

我正在尝试在fieldMapping中编写类似内容。但是我无法弄清楚如何使类型起作用,并且仍然使用对象在TypeScript和要委派的函数之间提供映射。

我意识到我可以使用switch语句或条件语句代替对象来映射事物,但我更愿意以这种方式进行操作。

type

2 个答案:

答案 0 :(得分:3)

恐怕您将不得不使用type assertion来避免此处的代码重复。 TypeScript的类型系统只是没有对这些"correlated record types"的良好支持,也没有依赖于两个联合类型的值(联合不是独立的)相互作用的任何操作。

您已经到达switch中的冗余代码解决方法;这是不安全声明的解决方法:

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

该函数采用联合类型的函数,例如((a: string)=>number) | ((a: number)=>boolean),并且不安全地将其缩小 到一个函数,该函数采用其参数类型的联合并返回其返回类型的联合,例如{ {1}}。这是不安全的,因为前一个联合类型的函数可能类似于((a: string | number) => string | number),但肯定匹配const f = Math.random()<0.5 ? ((a: string)=>a.length) : ((a: number)=>number.toFixed())。我无法安全地调用((a: string | number) => string | number),因为也许f(5)是字符串长度的函数。

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

renderFn

您对function renderFnAssertion(field: FormField) { const renderFn = assertNarrowFunction(fieldMapping[field.type]); renderFn(field.data); // okay } 的类型撒谎了……对它的接受并不是太多,它可以接受任何旧参数(例如,renderFn将根据需要失败),但是足够了它会允许这样做:

renderFn(123)

因此,您必须小心。

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

Link to code

答案 1 :(得分: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,
}

其他详细信息

Ryan Cavanaugh的

This GitHub comment启发了这种方法。当我们将FindByTypeswitch语句与tagged union / discriminated union类型结合使用时,if为我们提供了一些缩小类型的能力。

在给定相关输入后,与该注释相关的类型将如何扩展:

// 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'>;