组合索引泛型无法导出

时间:2019-07-12 21:36:51

标签: typescript

假设我具有以下两个组件,每个组件都使用属性接口来键入onChange方法:

export interface BorderValues {
  borderRadius: string;
  borderWidth: number;
  border: string;
}

export type BorderArguments = keyof BorderValues;

interface BorderProps {
  onChange: <T extends BorderArguments>(arg: T, val: BorderValues[T]) => void;
}

const Border = (props: BorderProps) => {
  props.onChange('borderRadius', '10px');
  // props.onChange('borderRadius', 10);  // error
  props.onChange('borderWidth', 10);
  // props.onChange('borderWidth', '10');  // error
  return 'Border';
}

export interface ContainerValues {
  padding: string | number;
  opacity: string | number;
  overflow: 'hidden' | 'visible';
}

export type ContainerArguments = keyof ContainerValues;

interface ContainerProps extends ContainerValues {
  onChange: <T extends ContainerArguments>(arg: T, val: ContainerValues[T]) => void;
}

const Container = (props: ContainerProps) => {
  // props.onChange('borderRadius', '10px');  // error
  props.onChange('padding', 10);
  props.onChange('overflow', 'hidden');
  // props.onChange('overflow', 10);  // error
  return 'container';
}

这一切都很好。现在,假设我想将它们组合起来...我无法将onChange设置为组合的setArgValue函数,因为每个属性都缺少属性:

type Arguments = BorderArguments | ContainerArguments;
interface Values extends BorderValues, ContainerValues {};

interface Props {
  // This works just fine.
  getArgValue: <T extends Arguments>(arg: T) => Values[T];
  // This obviously doesn't.
  setArgValue: <T extends Arguments>(arg: T, val: Values<T>) => void;
}

const Component = (props: Props) => {

  // error: onChange can't handle ContainerArguments.
  Border({onChange: props.setArgValue});

  // error: onChange can't handle BorderArguments.
  Container({onChange: props.setArgValue});
}

我试图创建一个派生类型,就像这样:

type Test<T extends Arguments> = 
  T extends BorderArguments ? BorderValues : 
  T extends ContainerArguments ? ContainerValues : 
  never;

但是这也不起作用,因为我实际上无法对其进行索引:

// This is a problem: I need the argument to both derive the correct collection,
// but also index it based on the `arg` parameter
setArgValue: <T extends Arguments>(arg: T, val: Test<T>) => void;

此外,如果我更改Test类型以返回索引处的项目,就像这样:

type Test<T extends Arguments> = 
  T extends BorderArguments ? BorderValues[T] : 
  T extends ContainerArguments ? ContainerValues[T] : 
  never;

Test始终等同于string | number但是,以下工作原理:

type Foo = Test<'overflow'>; // 'hidden' | 'visible'

到目前为止,我已经使用代码创建了playground

有什么方法可以完成我要定义的类型?预先感谢!

2 个答案:

答案 0 :(得分:1)

让我们备份并使用索引访问而不是条件类型。我还清理了一大堆与您的问题无关的(在我看来)多余的东西。鉴于此:

// BORDER COMPONENT
export interface BorderValues {
  borderRadius: string;
  borderWidth: number;
  border: string;
}
export type BorderArguments = keyof BorderValues;
interface BorderProps {
  onChange: <T extends BorderArguments>(arg: T, val: BorderValues[T]) => void;
}
const Border = (props: BorderProps) => {
  props.onChange("borderRadius", "10px");
  props.onChange("borderWidth", 10);
  return "Border";
};

// CONTAINER COMPONENT
export type ContainerArguments = keyof ContainerValues;
interface ContainerValues {
  padding: string | number;
  opacity: string | number;
  overflow: "hidden" | "visible";
}
interface ContainerProps {
  onChange: <T extends ContainerArguments>(
    arg: T,
    val: ContainerValues[T]
  ) => void;
}
const Container = (props: ContainerProps) => {
  props.onChange("padding", 10);
  props.onChange("overflow", "hidden");
  return "container";
};

// TEST COMBINATION:
type Arguments = BorderArguments | ContainerArguments;
interface Values extends BorderValues, ContainerValues {}
interface Props {
  setArgValue: <T extends Arguments>(arg: T, val: Values[T]) => void;
}

以下代码在TypeScript 3.4中有效,并在TypeScript 3.5中中断:

const Component = (props: Props) => {
  Border({ onChange: props.setArgValue }); // okay TS3.4, error TS3.5
  Container({ onChange: props.setArgValue }); //okay TS3.4, error TS3.5
};

这是由于breaking change in TS3.5引起的。索引访问写入的方式已经modified成为类型安全的。如果我有一个o类型的对象{a: A, b: B}并且我有一个联合类型为k的键"a" | "b",那么当我从{{1 }},我将获得一个联合类型o的值。在TS3.4及以下版本中,您可以一个类型为A | B的值。但这并不安全,因为如果我有一个联合类型为A | B的值v,并且我写了A | B,则o[k] = v很有可能是k和{ {1}}是"a",我做得不好。一种更严格但更安全的方法是要求写入联合密钥属性,我需要类型为v的交集值。这既是B,又是A & B,并且适合写入AB。这项更改使写入索引键变得安全。

令人沮丧。如果您执行类似o.a的操作,那么您期望能够正确地编写o.b。但是您意识到const v = o[k]的类型和o[k] = v的类型实际上是相互关联的,而不是彼此独立的……不幸的是,编译器是does not realize this。您想说k的类型为v,但是编译器从根本上认为它的类型为[k, v]。而且它抱怨。

您正在将["a", A] | ["b", B]分配给类型["a", A] | ["b", A] | ["a", B] | ["b", B]的变量也遇到相同的问题。


那么,我们如何处理呢?最简单的方法是使用type assertion,您只需要告诉编译器它的警告已被注意但不适用:

props.setArgValue

这可能是我的建议。


如果您愿意的话,还有一种更为冗长和round回的方式来处理此问题。看来它仍然可以write of a value that's been read from a generic index使用。因此,如果BorderProps["onChange"]是通用类型const ComponentAsserted = (props: Props) => { Border({ onChange: props.setArgValue as BorderProps["onChange"] }); Container({ onChange: props.setArgValue as ContainerProps["onChange"] }); }; ,则可以进行k而不会出错。

因此,我们可以通过使您的K extends "a" | "b"更通用然后再具体化来解决此问题:

const v = o[k]; o[k] = v;

props.setArgValue的编译是由于这种通用的限制。请注意,type Setter<T> = <K extends keyof T>(arg: K, val: T[K]) => void; const setterForFewerProps = <T extends U, U>( setter: Setter<T> ): Setter<Pick<T, keyof U>> => <P extends keyof U>(arg: P, val: T[P]) => setter(arg, val); setterForFewerProps,而类似props.setArgValue的事物是Setter<Values> setterForFewerProps(props.setArgValue)`:

borderProps.onChange

这可以很好地编译,并且可能比断言稍微更类型安全。但这是如此冗长,我不会打扰。


无论如何,希望能有所帮助;祝你好运!

Link to code

答案 1 :(得分:0)

偶然地,我能够使用Partiallink

解决此问题