如何从工会获得“可选”财产?

2024-01-01

这就是我想要实现的目标:

interface Point2d {
  x: number;
  y: number;
}

interface Point3d {
  x: number;
  y: number;
  z: number;
}

type Point = Point2d | Point3d;

const p: Point = getPoint();
const z: number | undefined = p.z;

然而,这是不可能的:TypeScript 将在最后一行出错z没有定义于Point2d。有什么办法可以让这个工作吗?


这个问题(尤其是讨论)引起了我的注意,这就是我设法做到的。

首先,我们可以尝试使用UnionToIntersection type https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type,获取类型包括all属性来自all联盟的要素。第一次尝试是这样的:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Intersect = UnionToIntersection<Point>;
type Keys = keyof Intersect;
function check<K extends Keys>(p: Point, key: K): p is Point & { [key in K]: Point[key] }  { 
    return key in p;
 };
function get<K extends Keys>(p: Point, key: K) {
    return check(p, key) ? p[key] : undefined;
};

这个想法如下:如果属性在那里,我们将限制类型,要求除了我们之前知道的之外,它确实在这里;然后返回这个类型。然而,这段代码无法编译:Type 'key' cannot be used to index type 'Point'.这在某种程度上是可以预料的,因为我们不能保证索引类型对于任何任意的情况都是正确的Point(毕竟,这是我们从一开始就试图解决的问题)。

修复方法相当简单:

function check<K extends Keys>(p: Point, key: K): p is Point & { [key in K]: Intersect[key] } { 
    return key in p;
 };
function get<K extends Keys>(p: Point, key: K) {
    return check(p, key) ? p[key] : undefined;
};
const z = get(p, 'z');
if (z) {
    console.log(z.toFixed()); // this typechecks - 'z' is of type 'number'
}

然而,这还没有结束。想象一下,由于某种原因我们需要存储Point3d x坐标为字符串,而不是数字。在这种情况下,上面的代码将失败(我将在此处粘贴整个块,因此我们不会将其与有问题的代码混合):

interface Point2d {
    x: number;
    y: number;
}

interface Point3d {
    x: string;
    y: number;
    z: number;
}

type Point = Point2d | Point3d;

function getPoint(): Point { throw ("unimplemented"); };
const p: Point = getPoint();

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Intersect = UnionToIntersection<Point>;
type Keys = keyof Intersect;

function check<K extends Keys>(p: Point, key: K): p is Point & { [key in K]: Intersect[key] } { 
    return key in p;
 };
function get<K extends Keys>(p: Point, key: K) {
    return check(p, key) ? p[key] : undefined;
};

const x = get(p, 'x');
if (x && typeof x === 'number') {
    console.log(x.toFixed()); // error: property 'toFixed' does not exist on type 'never'
}

问题是在十字路口xproperty必须同时是数字和字符串,这是不可能的,所以推断为never.

看来我们得想点别的办法了。让我们尝试描述一下我们想要的东西:

  • 对于每一个Key,存在于联盟的某些部分,
  • 以及对于每一个Part工会的,
  • 我们想要的类型是Part[Key],如果存在的话,或者undefined否则,
  • 我们想要绘制地图Key这些类型的联合Parts.

好吧,让我们首先将其字面翻译成类型系统:

type ValueOfUnion<T, K> = T extends any ? K extends keyof T ? T[K] : undefined : never;

这非常简单:

  • 对于联合中的任何类型T(这里我们使用联合分布),
  • 如果有一把钥匙K在其中,我们返回T[K],
  • 否则我们就返回undefined,
  • 并且 lase 子句只是一个语法部分,它永远不会成立(因为所有类型都扩展any).

现在,让我们继续创建映射:

type UnionMapping<T> = {
    [K in keyof UnionToIntersection<T>]: ValueOfUnion<T, K>;
}
type MappedPoint = UnionMapping<Point>;

同样,这一点很明确:对于任何键K存在于交集中(我们已经知道如何创建它),我们获取相应的值。

最后是吸气剂,它变得非常简单:

function get<K extends keyof MappedPoint>(p: Point, key: K) { 
    return (p as MappedPoint)[key]
};

这里的类型断言是正确的,因为每个具体Point is a MappedPoint。请注意,我们不能只要求这一点get函数接收一个MappedPoint,因为 TypeScript 会生气:

Argument of type 'Point' is not assignable to parameter of type 'UnionMapping<Point>'.
  Property 'z' is missing in type 'Point2d' but required in type 'UnionMapping<Point>'.

问题是映射失去了联合引入的可选性(用可能的undefined输出)。

简短的测试表明这确实有效:

const x = get(p, 'x'); // x is number | string - note that we got rid of unnecessary "undefined"
if (typeof x === 'number') {
    console.log(x.toFixed());
} else {
    console.log(x.toLowerCase());
}

const z = get(p, 'z'); // z is number | undefined, since it doesn't exist on some part of union
if (z) {
    console.log('Point 3d with z = ' + z.toFixed())
} else {
    console.log('Point2d')
}

希望有帮助!


Update:主题发起者的评论建议采用以下结构以进一步实现人体工程学:

type UnionMapping<T> = {
    [K in keyof UnionToIntersection<T> | keyof T]: ValueOfUnion<T, K>;
}
type UnionMerge<T> = Pick<UnionMapping<T>, keyof T> & Partial<UnionMapping<T>>;

function get<K extends keyof UnionMerge<Point>>(p: UnionMerge<Point>, key: K) { 
    return p[key];
};

这个想法是通过使属性真正可选(而不仅仅是允许传递undefined).

此外,使用柯里化的思想,我们可以立即将此构造推广到许多联合类型。在当前的公式中很难开始工作,因为我们无法显式传递一种类型并让 TypeScript 推断另一种类型,并且此函数声明推断类型T错误地:

function get<T, K extends keyof UnionMerge<Point>>(p: UnionMerge<T>, key: K) { 
    return p[key];
};
get(p, 'z'); // error, since T is inferred as {x: {}, y: {}}

但是,通过将函数分成两部分,我们可以显式提供联合类型:

function getter<T>(p: UnionMerge<T>) {
    return <K extends keyof UnionMerge<T>>(key: K) => p[key];
}

const pointGetter = getter<Point>(p);

现在已经通过了与上面相同的测试:

const x = pointGetter('x');
if (typeof x === 'number') {
    console.log(x.toFixed());
} else {
    console.log(x.toLowerCase());
}

const z = pointGetter('z');
if (z) {
    console.log('Point 3d with z = ' + z.toFixed())
} else {
    console.log('Point2d')
}

这可以在没有显式中间对象的情况下使用(尽管它看起来有点不寻常):

const x = getter<Point>(p)('x');
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何从工会获得“可选”财产? 的相关文章

随机推荐