如何定义一个可以是“A”或“B”但不能同时是“A”或“B”的类型?
示例:
type FromSnapshot = {
snapshotId: string;
};
type NewInstance = {
version: string;
storageGB: number;
};
type DatabaseConfig = FromSnapshot | NewInstance;
// Okay.
const fromSnapshot: DatabaseConfig = {
snapshotId: "123",
};
// Okay.
const newInstance: DatabaseConfig = {
version: "1.2.3",
storageGB: 20,
};
// Not okay -- object must specify `snapshotId` OR `version`, `storageGB` -- NEVER BOTH.
const fromSnapshotAndNewInstance: DatabaseConfig = {
snapshotId: "123",
version: "1.2.3",
storageGB: 20,
};
注意:目标是避免在每种类型中添加反向
never
属性(例如,将 snapshotId?: never
添加到 NewInstance
类型)。
我不确定你为什么需要这样做。
从上下文来看,我猜你想做这样的事情......数据库配置对象包括:
对于受歧视的工会和品牌来说,这似乎是一个很好的案例。
// apply a "brand" so that SnapshotIds and Versions still behave
// like strings, but are distinguishable to the type system
type SnapshotId = string & { __type: 'snapshotId' }
type Version = string & { __type: 'version'
// for the parts of the type that should remain distinct
// introduce a 'type' field with a literal value that varies for each type
type FromSnapshot = {
__type: 'from-snapshot'
snapshotId: string;
}
type NewInstance = {
__type: 'new-instance'
version: string;
}
type StorageCapacity {
storageGB: number;
}
type FromSnapshotDBConfig = StorageCapacity & FromSnapshot;
type NewInstanceDBConfig = StorageCapacity & NewInstance;
type DatabaseConfig = FromSnapshotDBConfig | NewInstanceDBConfig
现在 DatabaseConfig 将接受以下任一值:
const fromSnapshotConfig: DatabaseConfig = {
type: 'from-snapshot',
snapshotId: "snapshotId" as SnapshotId,
storageGB: 100
}
const newInstanceConfig: DatabaseConfig = {
type: 'new-instance',
version: '1.2.3' as Version,
storageGB: 200
}
但不是这个。
__type
字段充当鉴别器,告诉类型系统这实际上应该是联合类型的哪个成员,以及因此应该允许哪些其他字段
const typeErrorConfig: DatabaseConfig = {
type: 'new-instance',
version: 'snapshotId' as SnapshotId,
storageGB: 300
}
为了简化品牌字符串的处理,您可以创建非常简单的工厂函数:
const asVersion = (value: string) => value as Version;
然后你可以这样写:
const newInstanceConfig: DatabaseConfig = {
type: 'new-instance',
version: asVersion('1.2.3'),
storageGB: 200
}
或者,为了进一步简化它,您可以为整个配置对象创建工厂方法,这使得设置 __type 字段和品牌是隐式的。
您可以创建这些工厂函数:
const newInstanceDBConfigFactory = ({version, storageGB}: {version: string, storageGB: number}) => ({
__type: "new-instance", // setting __type in the factory means you don't have to explicitly set it when you use it
version,
storageGB
} as NewInstanceDBConfig); // cast to NewInstanceDBConfig handles setting the Version branded string type
const fromSnapshotDBConfigFactory = ({snapshotId, storageGB}: {snapshotId: string, storageGB: number}) => ({
__type: "from-snapshot",
snapshotId,
storageGB
} as FromSnapshotDBConfig); // cast to FromSnapshotDBConfig handles setting the SnapshotId branded string type
并像这样使用它们:
const fromSnapshotConfig: DatabaseConfig = fromSnapshotDBConfigFactory({
snapshotId: "snapshotId",
storageGB: 100
})
const newInstanceConfig: DatabaseConfig = newInstanceDBConfigFactory({
version: '1.2.3',
storageGB: 200
});
现在所有类型安全的优点都是隐式的,并且可以在代码中节省大量样板文件。
您甚至可以进一步简化,您可以使用工厂函数的返回类型来隐式定义它们,而不是直接定义 Config 类型。
经过所有的调整和优化,我最终得到的是这样的:
type SnapshotId = string & {__type: 'SnapshotId'}
type Version = string & {_type: 'Version'}
const newInstanceDBConfigFactory = ({version, storageGB}: {version: string, storageGB: number}) => ({
__type: "new-instance" as const, // constrain this to be a literal value, so it will not accept just any string
version: version as Version, // make sure to cast to the branded string variant
storageGB
});
const fromSnapshotDBConfigFactory = ({snapshotId, storageGB}: {snapshotId: string, storageGB: number}) => ({
__type: "from-snapshot" as const, // constrain to be a literal value
snapshotId: snapshotId as SnapshotId, // cast to branded string
storageGB
});
type DatabaseConfig = ReturnType<typeof fromSnapshotDBConfigFactory> | ReturnType<typeof newInstanceDBConfigFactory>
const fromSnapshotConfig: DatabaseConfig = fromSnapshotDBConfigFactory({
snapshotId: "snapshotId" as SnapshotId,
storageGB: 100
})
const newInstanceConfig: DatabaseConfig = newInstanceDBConfigFactory({
version: '1.2.3',
storageGB: 200
});
这仍然会产生 TypeError,正如预期的那样:
const typeErrorConfig: DatabaseConfig = {
__type: 'new-instance',
version: 'snapshotId' as SnapshotId,
storageGB: 300
}