typescript - 如何为采用 Partial<T> 和对象的交集参数的函数编写类型保护?

标签 typescript directus typeguards

我正在将 Directus API 与 Typescript 结合使用。使用 Typescript,其 API调用函数返回部分实体(例如 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});

谢谢。

最佳答案

您通过检查 id 来期待这一点Partial<Book> 的属性对象 truthiness ,你会得到编译器 narrow来自 Partial<Book> 的对象至Partial<Book> & {id: string} 。不幸的是,这并不是 TypeScript 中缩小的工作方式。请参阅microsoft/TypeScript#16976请求(长期的)开放功能请求来支持此类事情。

目前,如果您检查像 b.id 这样的属性的值,它只会缩小属性的类型b.id本身,而不是包含对象的类型 b ...好吧,除非b属于 discriminated union类型和 id是它的判别性质。但是Partial<Book>不是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} 的类型(又名 Book )。因此您可以调用Library.addBook(obj)没有错误。所以你已经放弃缩小范围b ,而是构建 b 的新版本已经缩小的类型。


如果您不想创建一个本质上与旧对象等效的新对象,您可以放弃检查 if (!b.id) {}而是写 user-defined type function这需要b作为输入并返回一个类型谓词,表示 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属性(或至少一个可选属性),并返回类型谓词 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">> 。这也相当于 Book (类型 Required<Pick<Partial<Book>, "id">> 很丑陋,用 Required<T> Pick<T, K> Partial<T> 实用程序类型编写,但如果你仔细检查它,你会发现它相当于 {id: string} )。

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


Playground link to code

关于typescript - 如何为采用 Partial<T> 和对象的交集参数的函数编写类型保护?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70745598/

相关文章:

typescript - AWS CDK 使用不同的管道部署相同的资源和相同的堆栈 | typescript

javascript - Directus API SDK : How to get items with M2M filter

python - 如何在 Python 中对对象的属性执行类型保护

typescript - 为什么无效合并运算符不能作为 typescript 中的打字员工作?

TypeScript v2.3.1 破坏了单元测试

angular - 使用 angular-map 包在 Angular 6 中集成 Bingmap

angular - PrimeNG p-calendar - disableDate 格式

javascript - 如何使用 Directus Javascript SDK 进行条件过滤?

directus - 下载存储文件时如何修复 Directus 错误代码 3

javascript - Typescript 中的类型保护正在缩小到从不键入