这个问题(尤其是讨论)引起了我的注意,这就是我设法做到的。
首先,我们可以尝试使用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'
}
问题是在十字路口x
property必须同时是数字和字符串,这是不可能的,所以推断为never
.
看来我们得想点别的办法了。让我们尝试描述一下我们想要的东西:
- 对于每一个
Key
,存在于联盟的某些部分,
- 以及对于每一个
Part
工会的,
- 我们想要的类型是
Part[Key]
,如果存在的话,或者undefined
否则,
- 我们想要绘制地图
Key
这些类型的联合Part
s.
好吧,让我们首先将其字面翻译成类型系统:
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');