我正在将 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
,因为它自己不知道如何做到这一点。
关于typescript - 如何为采用 Partial<T> 和对象的交集参数的函数编写类型保护?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70745598/