问题描述
我的React + Typescript项目遇到了一些问题。
每当我必须输入以null
开头的变量并将接收到某种类型的async
数据时,就会发生这种情况。
例如:我有一个AdminBlogPostPage
,管理员可用来创建和编辑帖子。
但是当第1页呈现时,仍然没有任何可用数据,因为该帖子将被加载async
。
当前,我正在这样输入(我正在使用--strictnullchecks
):
type ADMIN_BLOGPOST_STATE = {
blogPost: null | BLOGPOST
}
blogPost
状态从null
开始,一旦加载,它就变成BLOGPOST
。
工作正常。但是从这时起,管理员对blogPost
对象所做的一切都会变得很痛苦,因为Typescript总是会“担心” blogPost
可能是null
。>
在这种情况下,我通常使用Redux,因此在我的reducer
中,我经常需要进行类型断言,以便Typescript清楚地知道,当该动作发生时,blogPost
将是{ {1}},而不是BLOGPOST
。
例如:
null
人们通常如何处理/* ############################# */
/* #### BLOGPOST PROPERTIES #### */
/* ############################# */
UPDATE_CATEGORY(state,action:UPDATE_CATEGORY) {
const blogPost = state.blogPost as TYPES.BLOGPOST; // TYPE ASSERTION HERE
const { value } = action.payload;
blogPost.category = value;
},UPDATE_BOOLEAN_PROPERTY(state,action: UPDATE_BOOLEAN_PROPERTY) {
const blogPost = state.blogPost as TYPES.BLOGPOST; // TYPE ASSERTION HERE
const { name,value } = action.payload;
blogPost[name] = value;
},UPDATE_STRING_PROPERTY(state,action: UPDATE_STRING_PROPERTY) {
const blogPost = state.blogPost as TYPES.BLOGPOST; // TYPE ASSERTION HERE
const { name,UPDATE_PRODUCT_CATEGORY(state,action: UPDATE_PRODUCT_CATEGORY) {
const blogPost = state.blogPost as TYPES.BLOGPOST; // TYPE ASSERTION HERE
const { value } = action.payload;
blogPost.productCategory = value;
},
数据的类型?在数据到达之前以async
开头是否值得?还是最好以有效的null
存根作为初始状态,以便Typescript知道blogPost: BLOGPOST
始终都是blogPost
,而我不必处理这个重复的类型断言了吗?
注意:即使我决定从有效的BLOGPOST
开始,我仍然需要从数据库加载数据。不会使用初始状态。只是为了允许我使用blogPost: BLOGPOST
而不是blogPost: BLOGPOST
。
解决方法
这通常是一个令人讨厌的问题。这可能无法以您喜欢的方式充分解决您的问题,但是我还是将答案留在这里,希望对您有所帮助。
根据我的经验,我一直使用以下两种方法之一来处理此问题(其中一种是依赖于框架的):
-
在更加肯定的情况下使用更简洁的编译时类型断言,例如compile-time non-null assertion post-fix operator
!
(而不是显式强制转换)。 em>值是非null / non-undefined。例如,尽管state.blogPost
在运行时可能为空,但这会编译:state.blogPost![name] = value state.blogPost!.property = value
这断言
state.blogPost
的值不为空,应该可以正常编译。请注意,该操作符在编译过程中将被擦除,并且仅用于编译时类型检查。当确认值的类型可能为空时,应使用运算符,但从不应在运行时使用。如果该值在运行时为空,则当然会出现错误。此外,optional chaining在某些特定情况下也可能有用。 -
对于某些React框架,例如Next.js(SSR + CSR),可以通过API异步获取用于呈现页面的初始属性,并且直到{{1 }}通过道具解决。例如,此Next.js' documentation on
getInitialProps(context)
涵盖了这种情况。这是使用Next.js的特定示例,但原理是在插入组件的属性和呈现之前获取博客文章,并且在运行时和编译时不应为空。
与您的问题无关,请记住使用方括号Promise
的用例以及任何安全隐患。请参阅此GitHub文章,其中介绍了边缘情况下的潜在安全问题:https://github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square-bracket-notation.md
问题在于打字稿是正确的。您的应用程序中有一段时间状态为null
。
如果您在API调用完成之前尝试访问状态,将会发生什么?打字稿保护您免受此侵害。
您需要对此进行大量检查,但这可能是添加装载程序的好机会。
type BLOGPOST = {name: string};
type ADMIN_BLOGPOST_STATE = {
blogPost: null | BLOGPOST
}
const state:ADMIN_BLOGPOST_STATE = {
blogPost: null
}
// .. api calls eventually set state.blogPost to some valid value
if (state.blogPost === null) {
return <Loader/>
} else {
return <BlogPost id={state.blogPost.id} title={state.blogPost.title}/>
}
根据您的问题,我假设您不想这样做,并且您希望对此进行集中检查。
我还假设您要冒某些组件在加载程序之前访问状态的风险。在您的API调用运行时,您可能拥有一个包罗万象的加载器。
为解决此问题,我所做的就是将状态初始化为null
值,但让Typescript认为它是有效的。
type BLOGPOST = {name: string};
type ADMIN_BLOGPOST_STATE = {
blogPost: BLOGPOST // <-- no need for the null here
}
const state:ADMIN_BLOGPOST_STATE = {
blogPost: null! // <-- non-null assertion
};
// here,you could try to access state.blogPost.name and TS won't complain
// .. api calls eventually set state.blogPost to some valid value
// no need to check,since we assume it is always a valid value
return <BlogPost id={state.blogPost.id} title={state.blogPost.title}/>
这样做的好处是,一开始就将您的断言放在一个地方。
缺点是您在访问状态时需要格外小心。你自己一个人。
,我将创建一个单独的类型来表示尚未从后端/数据库加载数据的状态,而不是使用null
type UnloadedBlogPost {
stateKind: "unloaded";
}
type LoadedBlogPost {
stateKind: "loaded";
blogPost: BLOG_POST;
}
type BlogPostState = UnloadedBlogPost | LoadedBlogPost;
这可以让您在博客文章加载之前明确表示UI的状态,如果您想添加微调器或其他加载指示器,这可能会很有用。您的reducer仍然需要检查博客文章是否已加载,但是如果博客文章尚未加载,则尽早退出很简单:
UPDATE_CATEGORY(state,action:UPDATE_CATEGORY) {
// assuming state.blogPostState is of type BlogPostState...
if (state.blogPostState.stateKind === "unloaded") {
return state;
}
// state.blogPostState is narrowed to type LoadedBlogPost,state.blogPostState.blogPost can be accessed
},
作为补充,使用Redux时,通常希望您的reducer是纯函数,它们返回一个新的状态对象,而不是改变现有状态。参见the docs。
,我没有选择正确的答案,因为它们全都帮助我解决了这个问题。
无论如何,我最终要做的是:
- 我已经在使用
loading
布尔值来控制UI状态。因此,在数据到达之前,用户看到的唯一界面是<Spinner/>
。
我决定摆脱null
的值,并为blogPost
添加了一个空白的 valid 值作为初始状态。我的意思是空白,其中没有任何内容,但是具有所有必需的属性,并且完全实现了BLOGPOST
类型。
然后,我的类型如下:
type ADMIN_BLOGPOST_STATE = {
blogPost: BLOGPOST
}
这不仅帮助我摆脱了reducer中的类型断言,而且还使我可以编写更复杂的钩子来访问状态而不必担心类型断言。
export function useAdminBlogPostProperty<K extends keyof TYPES.BLOGPOST>(propName: K): TYPES.BLOGPOST[K] {
return useSelector((state: ROOT_STATE) => state.ADMIN_BLOGPOST.blogPost[propName]);
}
总而言之,async
状态将由loading
状态控制。因此,这就是阻止用户尝试更新尚未到达的数据的原因。 blogPost
最初是null
的事实,只是多余的。现在,我可以确信blogPost
将始终是BLOGPOST
,并且在编写假定具有该功能的代码时,Typescript不会发疯。
我刚刚想出了一个不同的方法来解决这个问题:
这是我的状态:
type ADMIN_BLOGPOST_STATE = {
blogPost: BLOGPOST
}
我会一直这样。因为我 100% 确定我所有将访问 blogPost
并期望 BLOGPOST
的组件仅在填充 blogPost
后才会呈现。
为了避免在初始状态分配期间必须创建一个虚拟状态来履行状态合约,我这样做:
initialNullState.ts
// THIS FUNCTION ALLOWS TO PASS NULL AND STILL OBEY
// THE STATE CONTRACT. BECAUSE WE KNOW FOR SURE THAT
// BECAUSE INITIAL STATE WILL NEVER BE ACCESSED
export const initialNullState = <T extends unknown>() : T => {
return null as T;
};
此函数传递 null
并对您需要的类型进行类型断言。
所以在我的初始状态(在我的 createSlice
调用中,因为我使用的是 @reduxjs/toolkit
,所以我这样做:
const getInitialState = (): PAGES.ADMIN_BLOGPOST => ({
status: { loading: true,error: null,notFound: false },blogPost: initialNullState(),});
initialNullState()
调用返回 null,但它会将其整形为 BLOGPOST
,因此 Typescript 不会抱怨。
当然,您必须 100% 确定调用您的状态的所有内容仅在页面完全加载并且确实存在 blogPost: BLOGPOST
后才进行这些调用。