正确使用 Typescript Set<T> 和交叉类型

问题描述 投票:0回答:2

我不明白为什么编译器会抱怨。以下是基本类型声明:

export enum SvgElementType {
  path = "path",
  circle = "circle",
}

export type SvgElement = {
  id: number;
  type: SvgElementType;
};

export type SvgPathElement = SvgElement & {
  type: SvgElementType.path;
  d: string;
};

export type SvgCircleElement = SvgElement & {
  type: SvgElementType.circle;
  cx: number;
  cy: number;
  radius: number;
};

export type Serializer<E extends SvgElement = SvgElement> = (element: E) => string;

const pathSerializer: Serializer<SvgPathElement> = e => "";
const circleSerializer: Serializer<SvgCircleElement> = e => "";

const serializers: Set<Serializer> = new Set();
serializers.add(pathSerializer); // <<<--- transpile error

// full TS error
// Argument of type 'Serializer<SvgPathElement>' is not assignable to parameter of type 'Serializer<SvgElement>'.
//  Type 'SvgElement' is not assignable to type 'SvgPathElement'.
//    Property 'd' is missing in type 'SvgElement' but required in type '{ type: SvgElementType.path; d: string; }'.ts(2345)

我发现的唯一方法是修改

Serializer
的声明,将
any
作为默认类型:

export type Serializer<E extends SvgElement = any> = (element: E) => string;

这标志着我可能有更好的方法来保留稍后设置迭代器使用的最少类型...

    

typescript generics set contravariance intersection-types
2个回答
1
投票
serializers

作为输入的函数,但随后你试图为其分配一个接受

SvgElement
的函数。
这是行不通的 - 它允许您将 

SvgPathElement

传递给需要

SvgElement
的函数。
其正式术语是“函数参数是

逆变

”。 无论如何,您的代码都需要针对正在序列化的对象进行运行时类型检查,因此您是否应该在没有泛型的情况下重新定义您的

SvgPathElement

类型?例如:

Serializer



1
投票

export type Serializer = (element: SvgElement) => string;

这将编译,因为集合 
function serialize(e: SvgElement) { for (const s of serializers) { return s(element); } }

的每个成员都被声明接受任何类型的

serializers
- 但这实际上并非如此,因为每个
SvgElement
只接受
Serializer 类型的元素,并且会失败如果使用不同类型调用,则会在运行时出现类型错误。

那么如何写出正确的类型呢?

在回答之前,让我们想象一下

serializers

将如何使用,因为我有点惊讶
serializers
是一个
Set
。我们如何在这个
Serializer
中找到正确的
Set
?在我看来,
Set
对于这个目的来说是一个非常糟糕的数据结构,而
Map
或字典对象将是一个更直接的选择。

因此,我会使用这样的东西:

export type SvgElementBase<T extends string> = { id: number; type: T; } export type SvgPathElement = SvgElementBase<"path"> & { d: string; }; export type SvgCircleElement = SvgElementBase<"circle"> & { cx: number; cy: number; radius: number; }; export type SvgElement = SvgPathElement | SvgCircleElement; export type SvgElementType = SvgElement["type"]; export type Serializer<E extends SvgElement> = (e: E) => string; export type Serializers = {[T in SvgElementType]: Serializer<SvgElement & SvgElementBase<T>>} const serializers: Serializers = { path: (e) => `path ${e.d}`, circle: (e) => `circle at (${e.cx},${e.cy}) with radius ${e.radius}`, } function lookupSerializer<E extends SvgElement>(type: E["type"]): Serializer<E> { // the things we do for type safety return serializers[type]; } export function serialize(element: SvgElement) { return lookupSerializer(element.type)(element); }
通过使用映射类型声明 

serializers

,我们可以表示键和值的类型是相关的,即为特定类型注册的序列化器仅接受该特定类型的值。通用 
lookupSerializer
 函数促使编译器实际分发联合,如果要内联它,编译器将不会分发联合,因此尽管实现很简单,但该函数实际上是必要的。

© www.soinside.com 2019 - 2024. All rights reserved.