网站搜索

在 TypeScript 中创建一个接口,其中仅允许两个属性中的一个?


假设我有一个接口

interface ICart {
    property1?: string,
    propert2?: string,
    someOtherProperty: string
}

我如何确保只允许 property1 和 property2 中的一个存在,但必须至少有一个存在?

答案#1

如果你想从列表中只允许一个属性,你需要创建一个对象类型的联合类型,每个对象类型允许某个特定属性并禁止其他所有属性。TypeScript 并不能完全禁止某个特定属性,但你可以做一些接近的事情:将其设为可选属性,其值类型为 never。实际上,这将允许该属性的类型为 undefined,但 undefined 属性和缺失属性之间的差别并不大(而且在使用普通编译器选项时,这种差别在 TypeScript 中并没有很好地体现,参见 ms/TS#13195)。

所以,对于你上面的例子,你想要的类型看起来像这样:

type ICartManual = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property1?: undefined;
    property2: string;
    someOtherProperty: string;
}

你可以验证它的行为是否符合你的预期:

const i1: ICartManual = {
    property1: "prop1",
    someOtherProperty: "other"
}

const i2: ICartManual = {
    property2: "prop2",
    someOtherProperty: "other"
}

const iBoth: ICartManual = { // error!
//    ~~~~~ <-- property1 is incompatible with undefined
    property1: "prop1",
    property2: "prop2",
    someOtherProperty: "other"
}

const iNeither: ICartManual = { // error!
//    ~~~~~~~~ <-- property2 is missing
    someOtherProperty: "other"
}

如果你有一个大型接口,并且想要取两个对象类型T和U,并创建一个新类型,该类型要求T中的恰好一个属性以及U中的所有属性,你可以这样定义它:

type OneKeyFrom<T, M = {}, K extends keyof T = keyof T> = K extends any ?
    (M & Pick<Required<T>, K> & Partial<Record<Exclude<keyof T, K>, never>>) extends infer O ?
    { [P in keyof O]: O[P] } : never : never;

这使用了大量的映射类型和条件类型来构建你想要的联合类型。我可以解释它是如何工作的,但需要很多文字。我以前做过类似的事情;可以在这里查看对类似类型的更深入描述。

无论如何,我们现在可以这样定义 ICart:

type ICart = OneKeyFrom<{ property1: string, property2: string }, { someOtherProperty: string }>;

你可以验证(例如通过 IntelliSense)它与手动编写的类型是相同的(除了属性的书写顺序,这不会改变类型):

/* type ICart = {
    property1: string;
    property2?: undefined;
    someOtherProperty: string;
} | {
    property2: string;
    property1?: undefined;
    someOtherProperty: string;
} */

代码链接

答案#2

// utility type which blocks two properties of the object coexisting 
type NeverTogether<A extends object, Key1 extends keyof A, Key2 extends keyof A extends Key1 ? never : keyof A> = 
  Omit<A, Key1 | Key2> & (({
    [k in Key1]: A[Key1]
  } & {[k in Key2]?: never}) | ({
    [k in Key1]?: never
  } & {[k in Key2]: A[Key2]}))

interface ICart {
    property1: string,
    property2: string,
    someOtherProperty: string
}

type IC = NeverTogether<ICart, 'property1', 'property2'>;

// error never together
const a: IC = {
  property1: '1',
  property2: '2',
  someOtherProperty: '2'
}

// error one needs to be there
const b: IC = {
  someOtherProperty: '2'
}

// correct
const c: IC = {
  property2: '2',
  someOtherProperty: '2'
}

// correct
const d: IC = {
  property1: '1',
  someOtherProperty: '2'
}

NeverTogether 类型的问题在于组合它以便对更多的键应用这样的规则。因此,它对两个相关字段效果很好,但对更多字段无法生效。但也许这会对你有帮助。对我来说,这是一个很有意思的谜题去解决。