在 TypeScript 中将接口的泛型联合属性缩小为就像它是一个局部变量一样
我有一个数据对象,我需要根据对象当前包含的联合成员将其传递到两个地方中的一个。两个地方都需要对象中的所有数据,所以用具体类型重新创建对象似乎有点傻。不过显然这样是可行的。
作为一种替代方法,我尝试让对象的接口在联合类型上泛型化,这样接口就可以在三个地方表示该对象,同时想着自动类型缩小可能会应用于 TestData 的类型参数。
interface UserData {
kind: 'user',
user: string,
}
interface ServerData {
kind: 'server',
url: string,
}
type DataTypes = UserData | ServerData
interface TestData<D extends DataTypes> {
data: D,
id: string,
}
现在顶级类可以使用 TestData<DataTypes>,而子类可以使用 TestData<UserData> 或 TestData<ServerData>。这在尝试将对象传递给其中一个子类之前都能正常工作。编译器会正确地缩小 TestData 的 data 属性的类型,但这并不会缩小实际对象的类型,实际对象的类型仍然是 TestData<DataTypes>。以下是一个示例。
function basicNarrow(test: TestData<DataTypes>) {
if (test.data.kind === 'user') {
// Correctly narrowed to `UserData`
test.data.user
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<UserData> = test
} else {
// Correctly narrowed to `ServerData`
test.data.url
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<ServerData> = test
}
}
在这一点上,我可以使用类型断言,或者(再次)创建一个新对象以传递正确的类型,但在一些查找之后,我找到了一个类似问题的答案,它给了我
type NarrowKind<T, N> = T extends { kind: N } ? T : never;
function predicateNarrow(test: TestData<DataTypes>) {
const predicate = <K extends DataTypes['kind']>(narrow: TestData<DataTypes>, kind: K): narrow is TestData<NarrowKind<DataTypes, K>> => (
narrow.data.kind === kind
)
if (predicate(test, 'user')) {
// Correctly narrowed to `UserData`
test.data.user
// Success! Generic type narrowed to `TestData<UserData>
const typed: TestData<UserData> = test
} else {
// Error: Not narrowed
test.data.url
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<ServerData> = test
}
}
这在 if 块内做了我想要的事情,但编译器不会在 else 块中缩小到另一种情况,除非像对待普通局部变量 data 那样进行另一次显式检查。
这是我希望最终缩小类型理想状态的一个例子
function idealNarrow(test: TestData<DataTypes>) {
function isKind(/*???*/) { /*???*/ }
if (isKind(test, 'user')) {
const user: UserData = test.data
const typed: TestData<UserData> = test
} else {
const server: ServerData = test.data
const typed: TestData<ServerData> = test
}
}
这两种解决方案都可以无问题地使用,但 predicateNarrow(...) 非常接近我想要的,有没有办法以某种方式将这两种行为结合起来,以在 else 块中自动缩小整个泛型 TestData<D> 类型?
答案#1
这里的问题是 TestData 本身并不是一个区分联合类型,只有包含的 data 属性的 D 是。换句话说,TS 可以通过 kind 判别器来缩小 data 的类型,而不能缩小外部的 TestData 类型。
predicate 只能检查 TestData 包含特定类型 UserData 或 ServerData,但它无法通过 if/else 块中的控制流推断其他可能的联合部分。可能的解决方案:
1) 缩小 DataTypes 并重新组合 TestData (代码)
function basicNarrow({ id, data }: TestData<DataTypes>) {
if (data.kind === 'user') {
data // UserData
const typed: TestData<UserData> = { id, data }
} else {
data // ServerData
const typed: TestData<ServerData> = { id, data }
}
}
2)使 TestData 本身成为一个判别联合(代码)
type DataTypes = UserData | ServerData
type TestData<D extends DataTypes> = D & { id: string }
function basicNarrow(test: TestData<DataTypes>) {
if (test.kind === 'user') {
test // UserData & { id: string; }
const typed: TestData<UserData> = test
} else {
test // ServerData & { id: string; }
const typed: TestData<ServerData> = test
}
}
