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

时间:2019-04-29 13:04:53

标签: typescript union-types

这是我想要实现的:

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;

但是,这是不可能的:在z上未定义Point2d的最后一行,TypeScript将出错。有什么办法可以使这项工作成功?

3 个答案:

答案 0 :(得分:3)

您可以使用类型防护来检查z的存在,例如

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

您可以看到此here的示例。

或者您可以通过以下功能使它更通用:

const get = (point: Point, prop: string): number => prop in point ? point[prop] : undefined

in关键字实际上是一个javascript运算符,但是在诸如此类的打字稿中经常使用它(例如,参见一个不错的示例here

答案 1 :(得分:2)

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

首先,我们可以尝试使用UnionToIntersection type,以从union的 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;
};

想法如下:如果属性存在,我们将限制类型,要求它确实在这里,除了我们之前知道的以外;然后只需返回此类型。但是,该代码无法编译:{{1​​}}这在某种程度上是可以预期的,因为我们不能保证索引类型对于任意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

似乎我们必须考虑其他方法。让我们尝试描述我们想要的东西:

  • 在联合的某个部分中存在的每个never
  • 并且对于联合的每个Key
  • 我们希望类型为Part(如果存在),否则为Part[Key]
  • 并且我们想将undefined的所有类型的Key映射到这些类型的并集。

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

Part

这很简单:

  • 对于联合type ValueOfUnion<T, K> = T extends any ? K extends keyof T ? T[K] : undefined : never; 中的任何类型(这里我们使用联合分布)
  • 如果其中包含密钥T,我们将返回K
  • 否则我们返回T[K]
  • 而lase子句只是一个语法部分,永远不会成立(因为所有类型都扩展了undefined)。

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

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。请注意,我们不能仅仅要求MappedPoint函数接受一个get,因为TypeScript会生气:

MappedPoint

问题在于,映射失去了联合引入的可选性(由可能的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')
}

这个想法是通过使属性成为真正的可选属性来消除不必要的类型断言(而不只是允许传递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]; }; )。

此外,使用currying的思想,我们可以一次将这种构造推广为许多联合类型。由于我们无法显式传递一种类型并让TypeScript推断另一种类型,并且此函数声明错误地推断出类型undefined,因此很难在当前的公式中工作:

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')
}

答案 2 :(得分:0)

我建议使用Discriminated Unions。与我见过的其他解决方案相比,这更简单并且提供了更强的键入功能。

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

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

type Point = Point2d | Point3d;

const p: Point = getPoint();
let z: number | undefined = undefined;
if (p.d === 3) {
  z = p.z;
}

如果您不想添加d属性,则可以使用z运算符简单地检查in是否存在。有关详情,请参见this documentation。这种解决方案不像使用区分联合那样健壮,尤其是对于更复杂的问题,但是可以解决像这样的简单问题。

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

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

type Point = Point2d | Point3d;

const p: Point = getPoint();
let z: number | undefined = undefined;
if ("z" in p) {
 z = p.z;
}