I've been running into some issues with my React + Typescript project.
This happens whenever I have to type a variable that starts of as null and will receive async data of some type.
For example: I have an AdminBlogPostPage that the admin uses to create and edit posts.
And I have a variable that will be used to keep the blogPost data.
But when that page 1st renders, there still won't be any data available, 'cause the post will be loaded async.
Currently, I'm typing it like this (I'm using --strictNullChecks):
type ADMIN_BLOGPOST_STATE = {
blogPost: null | BLOGPOST
}
The blogPost state starts off as null and once it's loaded, it becomes BLOGPOST.
It works fine. But from this point on, everything that the admin will do to the blogPost object becomes kind of a pain, because Typescript will always be "worried" that blogPost might be null.
I'm usually using Redux in those situations, so in my reducer I'm constantly having to do type assertions make it clear to Typescript that when that action happens, blogPost will be a BLOGPOST and not null.
For example:
/* ############################# */
/* #### 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, value } = action.payload;
blogPost[name] = value;
},
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;
},
How do people usually handle the types for async data? Is it worth it to start with null before the data arrives? Or is it best to start with a valid blogPost: BLOGPOST stub as the initial state so Typescript will know that blogPost will always be a BLOGPOST at all times and I won't have to deal with this repetitive type assertions anymore?
Note: Even if I decide to start off with the valid blogPost: BLOGPOST, I'll still need to load data from DB. The initial state will not be used. It will be there just to allow me to use blogPost: BLOGPOST instead of null | BLOGPOST.
This is generally an obnoxious issue. This might not sufficiently solve your question in the way you prefer, but I will leave my answer here anyways in the hopes it may help anyone.
From my experience, I have historically handled this with one of the two following ways (one of which is framework dependent):
Use a compile-time type assertion that is more concise, such as the compile-time non-null assertion post-fix operator
!(instead of an explicit cast) in scenarios where it is absolutely certain that the value is non-null/non-undefined. For example, despitestate.blogPostmight be nullish at runtime, this will compile:This asserts that the
state.blogPostvalue is non-null and it should compile fine. Note that this operator is erased during transpilation and is only for compile-time type checking. The operator should be used when it is acknowledged a value's type is potentially nullish, but it should never be at runtime. If the value is nullish at runtime, it will of course through an error. Additionally, optional chaining may also be useful in some specific scenarios.For some React frameworks, such as Next.js (SSR + CSR), initial properties to render the page can be fetched from an API asynchronously and the page will not complete its render until the
Promiseis resolved with the props. For example, this Next.js' documentation ongetInitialProps(context)covers this scenario.This is specific example using Next.js, but the principle idea is that the blog post would be fetched prior to injecting the component's properties and rendering, and should be non-null at runtime and compile-time.
Unrelated to your problem, keep in mind your use case for using square bracket notation
x[y]and any security implications. See this GitHub article explaining potential security issues in fringe cases: https://github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square-bracket-notation.md