网站搜索

在 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
    }
}