如何为接受参数的函数编写类型保护,该参数是 Partial<T> 和对象的交集?

How do I write a typeguard for a function that takes an argument that is an intersection of a Partial<T> and object?

我正在使用 Directus API 和 Typescript。使用 Typescript,它的 API 调用函数 return 部分实体(例如 PartialItem<Book>),所以我在继续传递数据之前努力检查是否存在所需的属性。

但是,我仍然遇到 Typescript 类型错误,这些错误与 Directus 具体无关,但可能与我处理部分类型的方式有关。

这是一个使用纯 Typescript (and on TS Playground) 的简化示例:

interface Book {
    id: string;
    title?: string;
    author?: string;
    pages?: number;
}

class Library {
    static books: Book[] = [];
    static addBook(data: Partial<Book> & {id: string}) {
        this.books.push(data);
    }
}

function fetchBooks(): Promise<Partial<Book>[]> {
    return new Promise((resolve)=>{
        setTimeout(()=>{
            resolve([
                {id: "1234",title:"Nineteen Eighty-Four"},
                {id: "2345", title: "The Great Gatsby", author: "F. Scott Fitzgerald"}
            ])
        },1000)
    })
}

function bookLoadingFunction() {
    fetchBooks().then(books=>{
        books.map(b=>{
            if(!b.id){
                // Handle the fact that the API is missing ID
            }else{
                Library.addBook(b); // * COMPILE ERROR HERE *
            }
        })
    })
}

即使在检查之后,编译器似乎也无法推断出 b.id 已定义。我收到以下错误:

Argument of type 'Partial<Book>' is not assignable to parameter of type 'Partial<Book> & { id: string; }'.
  Type 'Partial<Book>' is not assignable to type '{ id: string; }'.
    Types of property 'id' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.

这是编译器的限制,还是存在 b.id 确实仍然可以是 undefined 的边缘情况?有没有办法让编译器满意而不会失去类型安全性?

我知道以下内容会使错误消失,但它远非理想:

Library.addBook(b as Partial<Book> & {id: string});

谢谢。

您期望通过检查 Partial<Book> 对象的 id 属性 以获取 truthiness, you would get the compiler narrow the object from Partial<Book> to Partial<Book> & {id: string}. Unfortunately this is not how narrowing works in TypeScript. See microsoft/TypeScript#16976 的(长期存在的)开放功能请求来支持此类事情.

目前,如果您检查 属性 的值,如 b.id,它只会缩小 属性 b.id 本身的类型,而不是 b.id 的类型包含对象 b... 好吧,除非 b 完全属于 discriminated union type and id is its discriminant property. But Partial<Book> is not a union,更不用说被歧视的对象了。好吧。


以下是我能想到的解决方法。一种是通过 object spreading:

之类的方式从缩小的 属性 和对象的其余部分重新组装对象
if (!b.id) { } else {
    const obj = { ...b, id: b.id };
    /* const obj: {
        id: string;
        title?: string | undefined;
        author?: string | undefined;
        pages?: number | undefined;
    } */
    Library.addBook({ ...b, id: b.id }); // okay
}

可以看到obj被看作是等价于Partial<Book> & {id: string}的类型(a.k.a.Book)。因此,您可以毫无错误地调用 Library.addBook(obj)。因此,您已经放弃缩小 b,而是构建一个已经缩小类型的新版本 b


如果你不想创建一个本质上等同于旧对象的新对象,你可以放弃检查 if (!b.id) {} 而是编写一个 user-defined type function 需要 b 作为输入和 returns 一个 类型的谓词 b 可以缩小取决于结果是 true 还是 false.例如:

function hasId<T extends { id?: any }>(
  x: T
): x is T & Required<Pick<T, "id">> {
    return x.id !== undefined
}

hasId() 函数接受一个参数 x,已知该参数具有 id 属性(或至少一个可选参数),并且 returns 类型谓词 x is T & Required<Pick<T, "id">>。你可以看到它的实际效果:

if (!hasId(b)) { } else {
    b // b: Partial<Book> & Required<Pick<Partial<Book>, "id">>
    Library.addBook(b); // okay
}

else 子句中,hasId(b) 返回了 true,这意味着 b 已缩小为 Partial<Book> & Required<Pick<Partial<Book>, "id">>。这也等同于 BookRequired<Pick<Partial<Book>, "id">> 类型很难看,是根据 Required<T>, Pick<T, K> and Partial<T> 实用程序类型编写的,但如果你仔细研究它,你会发现它等同于 {id: string}).

所以在这里你决定告诉编译器如何缩小 b,因为它自己不知道如何这样做。


Playground link to code