TypeScript:接口的离散数量的部分和

jxct1oxe  于 2023-05-08  发布在  TypeScript
关注(0)|答案(1)|浏览(81)

我试图构造一个类型S,它是一个离散的接口数量的(部分)总和(在下面的例子中,AB),即:满足{} | A | B | (A & B)
我尝试了以下方法(使用预期值),但没有达到预期效果(很可能是因为我不知道如何表示除{}以外的空接口,我认为实际情况并非如此)。

type A = {
    readonly a1: 1
}

type B = {
    readonly b1: 1
    readonly b2: 2
}

type Maybe<T> = T | {}

type S =  Maybe<A> & Maybe<B>
// algebraic type: C = ({} & {}) | (A & {}) | (A & B) | ({} & B)

const value1: S = {} // should type-check (value1 contains neither A nor B)
const value2: S = {a1: 1} // should type-check (value2 contains A)
const value3: S = {b1: 1, b2: 2} // should type-check (value3 contains B)
const value4: S = {a1: 1, b1: 1, b2: 2} // should type-check (value4 contains A and B)
const value5: S = {d: 1} // ideally, should NOT type-check (`d` is an extraneous key)
const value6: S = {a1: 2} // should NOT type-check (value6 does not satisfy A)

这一点能否实现?
请注意,我曾考虑使用Partial,但它并不能完全解决我的问题,因为(Partial<A> & Partial<B>) !== ({} | A | B | (A & B))

vzgqcmou

vzgqcmou1#

TypeScript中的对象类型通常是 openextendible,并且允许值具有额外的属性。它们不是microsoft/TypeScript#12936中要求的 * 封闭 密封 * 或 “精确”。这种开放性通常非常有用,允许interface层次结构和class层次结构形成 type 层次结构。很好的是,子/超接口和子/超类也是子/超类型,如果你不能在不破坏类型的情况下在子接口或子类中添加属性,这将是令人讨厌的。
因此,例如,空类型{}并不意味着“没有 * 允许 * 属性的对象”;它的意思是“没有 * 已知 * 属性的对象”。所以{}将接受 * 几乎任何东西 *,(甚至是原始类型,因为它们是自动装箱的),除了nullundefined
(To TypeScript在object literals上执行过多的属性检查,原因与类型安全无关。所以const a: {x: number} = {x: 0, y: 1}会给予你一个错误,但是const b = {x: 0, y: 1}; const a: {x: number} = b;不会给你一个错误。还有弱类型检测,它对具有所有可选属性的类型做类似的事情。
因此,从技术上讲,要实现您所要求的内容,语言需要支持确切的类型。除非发生这种情况,否则我们必须尝试模拟或近似它。
一般来说,我们可以得到的最接近“没有 * 额外 * 属性的对象类型”是“没有 * 特定 * 属性集的对象类型”。嗯,即使是这样也不太可能。你能得到的最接近的结果更像是“一个对象类型,其中这个特定集合中的每个属性要么缺失,要么存在,但-undefined”。这可以通过一个mapped type来实现,它接受您想要禁止的所有键K,并使它们成为不可能的never类型的可选属性,如

type ProhibitKeys<K extends PropertyKey> = { [P in K]?: never }

如此给予

type Test = ProhibitKeys<keyof B>;
/* type Test = {
    b1?: undefined;
    b2?: undefined;
} */

我们有一个禁止在b1b2键上定义任何属性的类型。
回到Maybe<T>。而不是T | {},我们可以说T | ProhibitKeys<keyof T>

type Maybe<T> = T | ProhibitKeys<keyof T>;

因此,Maybe<T>要么具有T的 * 所有 *(必需)属性,要么 * 没有 * T的属性。现在,如果我们将S定义为

type S = Maybe<A> & Maybe<B>;

并使用How can I see the full expanded contract of a Typescript type?中描述的技术检查其结构:

type Expand<T> = T extends unknown ?
  { [K in keyof T]: T[K] } : never;

type ExpandS = Expand<S>;
/* type ExpandS = 
     { readonly a1: 1; readonly b1: 1; readonly b2: 2; } | 
     { readonly a1: 1; b1?: undefined; b2?: undefined; } | 
     { a1?: undefined; readonly b1: 1; readonly b2: 2; } | 
     { a1?: undefined; b1?: undefined; b2?: undefined; } */

您可以看到S具有四种有效的属性组合,并导致以下行为:

const value1: S = {} // okay, neither A nor B
const value2: S = { a1: 1 } // okay, A
const value3: S = { b1: 1, b2: 2 } // okay, B
const value4: S = { a1: 1, b1: 1, b2: 2 } // okay, both A and B

const value6: S = { a1: 2 } // error, this is a broken A
const value7: S = { a1: 1, b1: 1 }; // error, okay A but broken B

我想这些都是你想要的。您还可以在这里获得所需的错误:

const value5: S = { d: 1 } // error

但这是因为弱类型和过多属性检测,并且是脆弱的。你可能会惊讶于

const value5a = { d: 1, x: 2 };
const value5b: S & { x: number } = value5a; // no error

这会使弱类型检测(因为需要x)和过多的属性检查(因为value5a不是对象文字)失效。但这真的是我们能做的最好的了;归根结底,TypeScript * 没有确切的类型 *,所以像这样的近似值应该足够了。
Playground链接到代码

相关问题