定义类型为 A 或 B,而不是两者的并集/交集

问题描述 投票:0回答:1

如何定义一个可以是“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,
};

在 TS Playground 观看。

注意:目标是避免在每种类型中添加反向

never
属性(例如,将
snapshotId?: never
添加到
NewInstance
类型)。

typescript
1个回答
0
投票

我不确定你为什么需要这样做。

从上下文来看,我猜你想做这样的事情......数据库配置对象包括:

  • snapshotId 和 storageGB 属性 或
  • 版本和 storageGB 属性

对于受歧视的工会和品牌来说,这似乎是一个很好的案例。

// 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
}
© www.soinside.com 2019 - 2024. All rights reserved.