通用组件中的“ A可分配给类型B的约束,但B可以用不同的子类型实例化”

时间:2020-01-18 21:22:15

标签: reactjs typescript

我具有以下成分DataListBase

export type Page<T> = {
  total: number;
  items: T[];
};

type Filters = {
  search: string | null;
  sort: string | null;
  sortOrder: number | null;
  limit: number;
  page: number;
};

type DataListBaseProps<TData, TVariables> = {
  query: DocumentNode;
  variables: Omit<TVariables, "page">;
};

export const DataListBase = <
  TData extends Record<string, Page<TInner>>,
  TVariables extends Pick<Filters, "limit" | "page">,
  TInner = any
>({
  query,
  variables
}: DataListBaseProps<TData, TVariables>) => {
  const actualVariables: TVariables = useMemo(
    () => ({
      ...variables,
      page: 1
    }),
    [variables]
  );

  const result = useQuery<TData, TVariables>(query, {
    variables: actualVariables
  });

  // rest is not important
  return null;
}

actualVariables的声明中说:

Error:(22, 9) TS2322: Type 'Pick<TVariables, Exclude<keyof TVariables, "page">> & { page: number; }' is not assignable to type 'TVariables'.
  'Pick<TVariables, Exclude<keyof TVariables, "page">> & { page: number; }' is assignable to the constraint of type 'TVariables', but 'TVariables' could be instantiated with a different subtype of constraint 'Pick<Filters, "page" | "limit">'.

我看不出这有什么道理。 Pick<TVariables, Exclude<keyof TVariables, "page">> & { page: number; }实际上与TVariables完全相同。以我的理解,它只是删除了页面密钥,然后重新添加了它。这与我们必须开始的类型有何不同?以及我该如何更正我的代码?

谢谢。

1 个答案:

答案 0 :(得分:2)

您问题的minimum reproducible example可能是这样的:

function f<T extends { limit: number, page: number }>(
  t: T,
  x: Omit<T, "page"> & { page: number; }
): T {
  return x; // error!
}

编译器无法验证x的类型为T。实际上x可能不是{em> 类型的T,如以下示例所示:

interface PageSeven {
  limit: number,
  page: 7; // only seven, that's it
}
const page7: PageSeven = { limit: 123, page: 7 };
const oopsPage7 = f(page7, { limit: 456, page: 100 });
console.log(oopsPage7.page); // 7 at compile time, 100 at runtime, oops

编译器认为oopsPage7.pagenumeric literal type 7而不是较宽的number类型,但是在运行时oopsPage7.page将是{{1} }。糟糕!

如果对象类型为100,则类型V可以是该对象的子类型,可以添加新属性,也可以缩小现有属性的类型。在后一种情况下,您无法通过使用U extends V恢复V;您可能仍然拥有较窄的类型,例如上面的Pick<U, keyof V>


但是您要进行一些工作……事实证明,您可以表示类似于此的实际上有效的转换,这些转换是类型安全的,但是编译器 still 无法验证它:

Page7

这里,编译器仍在抱怨function g<T extends { limit: number, page: number }>( t: T, x: Omit<T, "page"> & Pick<T, "page"> ): T { return x; // still error! } g(page7, { limit: 456, page: 100 }); // error at compile time g(page7, page7); // okay ;即使return x类型确实确实可以分配给Omit<T, "page"> & Pick<T, "page">,因为即使缩小的T属性也将保留。这是known issue in TypeScript, see microsoft/TypeScript#28884。问题在于,编译器通常不会通过验证非泛型对象是否可以分配给依赖于未指定泛型类型参数的类型的工作。最终可能会解决此问题,但目前尚不清楚。即使可以正确完成它,也需要足够有效地完成它,以免显着降低编译器性能。如果我是你,我不会屏住呼吸。


那么,该怎么办?如果您确定不会像我在page中提到的那样缩小范围,或者您不关心它,则可以使用type assertionPageSeven)。这是在以下情况下的常见建议:在这种情况下,您知道自己在做的事情是类型安全的,而编译器却不是(请注意,您应该对安全性进行三重检查,因为您已经承担了验证它的责任) ):

expr as Type

另一方面,您可以注释函数,以便它们处理function f<T extends { limit: number, page: number }>( t: T, x: Omit<T, "page"> & { page: number; } ): T { return x as T; // no error, technically unsafe } function g<T extends { limit: number, page: number }>( t: T, x: Omit<T, "page"> & Pick<T, "page"> ): T { return x as T; // no error, probably safe } 而不是Omit<T, "page"> & {page: number}

T

无论哪种方法都可以。断言可能是最方便的,但可能(略)不安全,而重新注释可能会扩展到整个代码库,而安全性仅比断言稍微安全一些。不过,这取决于您。


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

Playground link to code