为什么在使用“推断”时第二泛型信息丢失了?

时间:2019-09-27 13:43:01

标签: typescript generics

因此,我有一个抽象基类,该基类接收一个ValueType的值,并具有一个能够将其转换为TransformedType的函数。这可以通过两个泛型来解决:

abstract class Base<ValueType, TransformedType> {
  public abstract transformValue: (value: ValueType) => TransformedType;
}

现在我有两个从Base适当扩展的类,一个用于ValueType字符串,另一个用于数字:

interface StringTransformed {
  someValue: string;
}

class DerivedString extends Base<string, StringTransformed> {
  public transformValue = (value: string): StringTransformed => {
    return { someValue: value + "someValue" };
  };
}
interface NumberTransformed {
  valuePlusPlus: number;
}

class DerivedNumber extends Base<number, NumberTransformed> {
  public transformValue = (value: number): NumberTransformed => {
    return { valuePlusPlus: value + 1 };
  };
}

此后,我创建了一个类型Fields,该类型需要类型T中的通用类型object

type Fields<T extends object> = {
  [A in keyof T]: any extends Base<T[A], infer P> ? Base<T[A], P>: never
};

用法示例:

type MainType = { x: string; y: number };

const fields: Fields<MainType> = {
  x:  new DerivedString("test"),
  y: new DerivedNumber(2)
};

const transformed = fields.x.transformValue("anystring");

这里的问题是关于TransformedType的类型信息丢失了,因为它是unknown

enter image description here enter image description here

与之相反,关于ValueType的信息保持正确。

问题:

  1. 如何在不增加任何限制的情况下正确采用通用类型?
    在这里使用infer是否正确?
    为什么类型信息会丢失?

  2. 我可以从TransformedType的通用参数中删除Base并只在transformValue函数中使用它吗?(ValueType是否需要其他函数等Base内,但TransformedType仅用于transformValue函数)

编辑:

根据评论的需要,我创建了一个codesandbox,其中包含用例的最小示例。基本上有一些我无法解决的any类型转换,这是我的实际问题。

总的来说,我想在React中编写一个用于处理表单状态的库,因此它确实需要是一种非常通用的方法。请记住,这个示例实际上确实简化了,实际上更加复杂。

1 个答案:

答案 0 :(得分:1)

好吧,对于第2个问题,首先:是的,如果这样定义,您基本上可以删除第二个类型参数,并将其默认设置为unknown

// default second parameter
abstract class Base<T, U = unknown> {
  public abstract transformValue: (value: T) => U;
}


interface StringTransformed {
  someValue: string;
}

class DerivedString extends Base<string> {
  public transformValue = (value: string): StringTransformed => {
    return { someValue: value + "someValue" };
  };
}

interface NumberTransformed {
  valuePlusPlus: number;
}

class DerivedNumber extends Base<number> {
  public transformValue = (value: number): NumberTransformed => {
    return { valuePlusPlus: value + 1 };
  };
}

实际的类DerivedStringDerivedNumber仍然知道其transformValue()方法返回的类型。


回到问题1 ...首先,您对Fields的定义可能也是这样:

type Fields<T = any> = { [K in keyof T]: Base<T[K]> };

几乎相同。请注意,我给了默认类型参数any,因此Fields本身就是Fields<any>

现在,正如我评论的那样,您实际上不需要注释任何特定的常量,因为它们具有某些特定的Fields<...>类型;类型推断将为您解决这个问题,如果您尝试将不好的事情传递到最终的transformFields()函数中,则会被警告。不过,如果您想及早收到有关常量错误的警告,则可以执行如下帮助函数:

const asFields = <F extends Fields>(fields: F) => fields;

如果您给它提供一个不匹配的值,它将发出抱怨。这样就可以了:

const fields = asFields({
  x: new DerivedString(),
  y: new DerivedNumber()
});

这会导致错误:

const badFields = asFields({
  x: "whoops", // error! string is not a valid Base 
  y: new DerivedNumber() 
})

编译器会记住fields的实际作用:

fields.x.transformValue("anystring").someValue; // okay

如果您有特定的类型T,则需要转换fields常量,则可以使一个curried辅助函数来手动指定T并推断出休息:

const asValueFields = <T>() => <F extends Fields<T>>(fields: F) => fields;

然后这有效:

const fieldsAlso = asValueFields<MainType>()({
  x: new DerivedString(),
  y: new DerivedNumber()
});

但这失败了:

const badFieldsAlso = asValueFields<MainType>()({
  x: new DerivedNumber(), // error! string is not number
  y: new DerivedString()  // error! number is not string
});

它仍然记住特定的输出类型:

fieldsAlso.x.transformValue("anystring").someValue; // okay

最后,让我们实现transformFields()。描述给定Fields类型的转换后的输出类型很有用。这是我的定义方式:

type TransformedType<F extends Fields> = {
  [K in keyof F]: F[K] extends Base<any, infer T> ? T : never
};

这可能是“如何防止类型丢失”解决方案的症结所在?上面的操作是使用类型F来确定其处理的输入和输出类型,然后为每个属性提取输出类型。

因此transformFields接受一个T和一个F并返回一个TransformedType<F>

const transformFields = <T extends object, F extends Fields<T>>(
  fields: F,
  values: T
): TransformedType<F> => {
  return (Object.keys(fields) as Array<keyof T>).reduce(
    <K extends keyof T>(transformedObj: TransformedType<F>, key: K) => ({
      ...transformedObj,
      [key]: (fields[key] as Base<T[K], TransformedType<F>[K]>).transformValue(
        values[key]
      )
    }),
    {} as TransformedType<F>
  );
};

该实现与您的代码沙箱中的实现相同,但其中的一些经过仔细放置type assertions以使编译器确信您正在执行的操作是可以的。基本上,我们断言Object.keys(fields)将返回T的键数组,fields[key]是一个Base,它接受​​一个T[K]并输出一个{{ 1}},并且TransformedType<F>[K]累加器是reduce()

哇!让我们使用它:

TransformedType<F>

看起来不错。请注意,您不必使用之前的辅助功能来“准备” const transformedMainType = transformFields(fields, { x: "", y: 1 }); transformedMainType.x.someValue; // okay transformedMainType.y.valuePlusPlus; // okay 。如果您有正确键入的对象文字,它也可以正常工作:

fields

好的,很多。希望能给您一些指导。祝你好运!

Link to code