泛型 - 正确使用 Typescript Set<T> 与交叉类型
我不明白为什么转译器会报错。以下是基本的类型声明:
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;
这提醒我,可能有更好的方法来在之后使用 serializers 集合迭代器时保持最小的类型定义...
答案#1
转译器报错是因为你实际上是在说你想要一个以 SvgElement 作为输入的函数,但随后你却试图将一个以 SvgPathElement 作为输入的函数赋给它。
这行不通——它会允许你将一个 SvgElement 传递给需要 SvgPathElement 的函数。
这个正式术语是“函数参数是逆变的”。
您的代码无论如何都需要对正在序列化的对象进行运行时类型检查,所以您是否应该考虑重新定义您的 Serializer 类型而不使用泛型?例如:
export type Serializer = (element: SvgElement) => string;
答案#2
编译器正在报错,因为如果允许你的代码编译,它可能会在运行时导致类型错误。例如,如果有人这样做:
function serialize(e: SvgElement) {
for (const s of serializers) {
return s(element);
}
}
那将可以编译,因为集合 serializers 的每个成员都声明为接受任何类型的 SvgElement——但实际上并非如此,因为每个 Serializer 只接受特定类型的元素,如果用不同类型的元素调用,将在运行时出现类型错误。
那么你如何写出正确的类型?
在我回答之前,让我们先想象一下 serializers 将如何使用,因为我有点惊讶 serializers 是一个 Set。我们如何在这个 Set 中找到合适的 Serializer?在我看来,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 函数会促使编译器实际分配联合类型,如果你将其内联,编译器不会分配联合类型,因此尽管函数实现很简单,仍然是必要的。
