What are the recommended data types for variables that will be used to store asynchronous data from a database or API while utilizing strict null checks in TypeScript

I'm currently facing issues with my React and Typescript project. Specifically, I encounter problems when dealing with variables that are initially set to null and will later receive asynchronous data of a certain type.

For instance, let's say I have an AdminBlogPostPage where the admin can create and edit posts. There's a variable used to store the blog post data, which starts off as null because the post will be loaded asynchronously.

To handle this scenario under strict null checks, I define the type like this:

type ADMIN_BLOGPOST_STATE = {
  blogPost: null | BLOGPOST
}

The blogPost state transitions from being null to holding a BLOGPOST once loaded. Although functional, it leads to inconvenience as Typescript constantly worries about potential null values for blogPost.

In such cases, I typically use Redux and find myself repeatedly performing type assertions in my reducers to clarify to Typescript that after certain actions, blogPost is guaranteed to be a BLOGPOST and not null.

How do others manage the typing for asynchronous data? Is it better to start with null and update it later or begin with a valid blogPost: BLOGPOST stub as the initial state to avoid repetitive type assertions?

Note: Even if I opt for the latter approach, whereby blogPost is initialized as a BLOGPOST, I still need to fetch data from the database. The initial state serves merely to indicate to Typescript that blogPost will always be a BLOGPOST.

Answer №1

The problem here lies in the fact that Typescript is correct. At some point in your application, the state will be null.

Imagine what would occur if you attempted to access the state before the API call had completed. Typescript safeguards against this.

You'll need numerous checks for this scenario, but it presents an opportunity to incorporate loaders.

type BLOGPOST = {name: string};
type ADMIN_BLOGPOST_STATE = {
  blogPost: null | BLOGPOST
}
const state:ADMIN_BLOGPOST_STATE = {
  blogPost: null
}

// .. eventually, API calls will update state.blogPost with a valid value

if (state.blogPost === null) {
   return <Loader/>
} else {
   return <BlogPost id={state.blogPost.id} title={state.blogPost.title}/>
}

Based on your query, I assume you are not inclined towards this approach and instead prefer a centralized check for such situations.

I also presume that you are willing to take the risk of a component accessing the state prematurely while implementing a catch-all loader during API calls.

One method I've used to tackle this issue in the past is initializing the state with a null value but convincing Typescript it's valid.

Typescript Playground

type BLOGPOST = {name: string};
type ADMIN_BLOGPOST_STATE = {
  blogPost: BLOGPOST // <-- no need for null here
}
const state:ADMIN_BLOGPOST_STATE = {
  blogPost: null! // <-- non-null assertion
};

// at this point, you can attempt to access state.blogPost.name without TS errors

// .. API calls eventually set state.blogPost to a valid value

// no need to validate since we assume it's always valid
return <BlogPost id={state.blogPost.id} title={state.blogPost.title}/>

The benefit of this approach is consolidating your assertions at the beginning.

The drawback is needing to be extra cautious about how you interact with your state. It's up to you to handle it carefully.

Answer №2

Dealing with this issue can be quite frustrating. While my answer may not completely resolve your question as you would like, I will still provide it in the hopes that it may assist someone.

In my experience, I have encountered and managed this issue in two ways, one of which is dependent on the framework being used:

  • To handle scenarios where a value is certain to be non-null or non-undefined, consider using a compile-time type assertion like the compile-time non-null assertion post-fix operator !. This avoids explicit casting and helps with concise code. For example, even if state.blogPost could be null at runtime, you can use:

    state.blogPost![name] = value
    state.blogPost!.property = value
    

    This asserts that the state.blogPost value is non-null during compilation. Remember, this operator is only for compile-time checking and will throw an error at runtime if the value is nullish. In some cases, optional chaining can also be beneficial.

  • In certain React frameworks like Next.js (SSR + CSR), initial properties needed for rendering can be fetched asynchronously from an API. The page will wait until the Promise resolves with the props before completing its render. Check out Next.js' documentation on getInitialProps(context) for more information on this approach.

    This example focuses on Next.js, but the concept applies to ensuring that the blog post is fetched prior to rendering to avoid nullish values both at compile-time and runtime.


On a side note, when using square bracket notation like x[y], consider any security implications. Consult this GitHub article discussing potential security issues in specific situations: https://github.com/nodesecurity/eslint-plugin-security/blob/master/docs/the-dangers-of-square-bracket-notation.md

Answer №3

Instead of relying on null, consider creating a distinct type to depict the state where data is still pending from the backend/database, like so:

type UnfetchedBlogPost {
  status: "unfetched";
}

type FetchedBlogPost {
  status: "fetched";
  post: BLOG_POST;
}

type BlogPostStatus = UnfetchedBlogPost | FetchedBlogPost;

This approach allows you to clearly indicate the UI's state prior to the blog post being loaded, which could be handy for incorporating a loading spinner or similar indicator. While your reducers will still need to check if the blog post is loaded, you can easily handle the scenario where it's not:

UPDATE_CATEGORY(state, action:UPDATE_CATEGORY) {
  // assuming state.blogPostStatus is of type BlogPostStatus...
  if (state.blogPostStatus.status === "unfetched") {
    return state;
  }

  // The state now refers specifically to a LoadedBlogPost, enabling access to state.blogPostStatus.post
},

Additionally, when working with Redux, it's advisable for your reducers to be pure functions that generate a new state object instead of modifying the existing state directly. Refer to the documentation for more information.

Answer №4

Instead of selecting a single correct answer, I found value in all the assistance provided to help me understand this concept.

In my approach, I made sure to handle UI status using a boolean variable called loading. This meant that before any data was loaded, the user would only see a loading spinner on the interface.

  • To streamline the process, I decided to eliminate the null value and introduced an empty valid value for the initial state of blogPost. This empty value did not contain any content but had all the necessary properties and fully adhered to the BLOGPOST type.

This led to a new type definition:

type ADMIN_BLOGPOST_STATE = {
  blogPost: BLOGPOST
}

By making these changes, I eliminated the need for type assertions in the reducer and enabled the creation of more advanced hooks for accessing the state without concerns about type inconsistencies.

export function useAdminBlogPostProperty<K extends keyof TYPES.BLOGPOST>(propName: K): TYPES.BLOGPOST[K] {
  return useSelector((state: ROOT_STATE) => state.ADMIN_BLOGPOST.blogPost[propName]);
}

In summary, the loading status effectively controls the asynchronous behavior, preventing users from updating data that hasn't loaded yet. Removing the initial null value from blogPost ensured that it will always be a valid BLOGPOST, providing stability when writing code in Typescript.

Answer №5

I've recently devised a fresh approach to tackle this particular issue:

Allow me to present my state shape:

type ADMIN_BLOGPOST_STATE = {
  blogPost: BLOGPOST
}

I intend to stick with this structure because I am confident that all components accessing blogPost and expecting BLOGPOST will only be rendered after blogPost has been populated.

In order to bypass the need for creating a placeholder state to fulfill the initial state requirement, I have implemented the following method:

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;
};

This function utilizes null but performs a type assertion for the required type.

Therefore, during the initial state setup (within my createSlice call, since I utilize @reduxjs/toolkit), I proceed in the following manner:

const getInitialState = (): PAGES.ADMIN_BLOGPOST => ({
  status: { loading: true, error: null, notFound: false },
  blogPost: initialNullState(),
});

The invocation of initialNullState() yields null, however, it conforms to the shape of BLOGPOST, thereby preventing any complaints from Typescript.

Naturally, it is imperative to ensure that every instance invoking your state does so only after the page has fully loaded and a blogPost: BLOGPOST object is truly available.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Error Alert: Next.js TypeScript is reporting that the necessary packages are missing from your setup

As I work on developing a basic Next.js website using their TypeScript starter, everything was going smoothly with the 'yarn dev' command. However, out of nowhere, I started encountering an error message whenever I tried to run 'yarn dev&apo ...

When attempting to import TypeScript types and interfaces alongside Svelte components, a compiler error occurs during the import process

Currently, I am working on a Svelte project that utilizes Vite as the primary build tool. I encountered an issue while trying to import a TypeScript interface (toDo) into a Svelte component. The problem arose during compilation of the component. Below is ...

How can I leverage the data fetched from API in Angular 13?

Just dipping my toes into the world of Angular by creating a quiz app to gain some hands-on experience. Successfully receiving a random set of questions from the API, but now facing the challenge of iterating over this array to implement the gameplay. The ...

Angular does not seem to support drop and drag events in fullCalendar

I am looking to enhance my fullCalendar by adding a drag and drop feature for the events. This feature will allow users to easily move events within the calendar to different days and times. Below is the HTML code I currently have: <p-fullCalendar deep ...

Inoperable await statement within a while loop

Here is my code snippet for the app: const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); async function init() { while (true) { console.log("TICK"); await (rl.qu ...

What steps can I take to ensure the reliable functioning of this npm script?

Within my npm package, I have these two straightforward scripts: "prebuild": "rimraf dist types", "build": "tsc", Development dependencies rimraf@^5.0.5 and typescript@^5.3.2 are both installed. However, when I run ...

Executing Timers in Angular 5 Service

I am working on implementing a property called currentAndLastVehicles in my service that needs to be updated automatically every second. Here is what I have so far: import { Injectable } from '@angular/core'; @Injectable() export class SharedD ...

Facing compilation issues when using Typescript with Svelte

My project was set up using npm create svelte@latest, with ts enabled and tailwind added. However, when declaring <script lang="ts">, I encountered an Unexpected Token error, like the one below: D:/gits/rpg-board/src/lib/components/FilterB ...

The Vue Prop does not have an initializer and is not explicitly assigned in the constructor

Currently, I am utilizing props in Vue Class components. Within the constructor, I have defined the props without a value. This setup compiles and operates smoothly, except after the most recent VS Code / TSLint update, a warning message emerges: The pr ...

Angular2 Error: Issue with the "match" function in JavaScript

Why am I receiving a typeerror that says "cannot read property of 'match' undefined"? var numInput = document.getElementById('input'); // Listen for input event on numInput. numInput.addEventListener('input', function(){ ...

how to use all parameters except the first one in TypeScript

Is there a way to reference one function's parameter types in another function, but only use a subset of them without repeating the entire list of parameters? //params of bar should be same as foo, except p1 should be a different type function foo(p1 ...

Ensuring Consistency in Array Lengths of Two Props in a Functional Component using TypeScript

Is there a way to ensure that two separate arrays passed as props to a functional component in React have the same length using TypeScript, triggering an error if they do not match? For instance, when utilizing this component within other components, it sh ...

Empty Angular-chart.js Container

How can I resolve the issue of getting a blank div and no output while trying to display a chart where the options, labels, and other data are initialized in the TypeScript controller and then used on the HTML page? I added the angular-chart.js library us ...

Obtain the identifier for the navigation hyperlink

I need to dynamically add an ID using JavaScript @Component({ selector: 'kit-general-16', templateUrl: './16.component.html', styleUrls: ['./16.component.scss'], }) export class CuiGeneral16Component implements OnChanges ...

What specific data type is required for the following code in Angular?

const componentMapper = { input: InputComponent, button: ButtonComponent, select: SelectComponent, date: DateComponent, radiobutton: RadiobuttonComponent, checkbox: CheckboxComponent, switch: SwitchComponent, textarea: TextAreaComponent }; ...

Load jQuery core asynchronously with a backup plan

Exploring performance optimization and non-blocking scripts in the header, my focus has been on asynchronously loading jQuery itself. In my search, I came across a jQuery Loader script that can async load jQuery and then handle and queue jQuery document r ...

Still trying to wrap my head around Node.js, definitely a newbie in that department

REVISED: I have made changes to my storyline and tried adding output to the code as demonstrated in examples, but it doesn't seem to work. What am I missing? Greetings to all the experts and enthusiasts out there, The question at hand is "how can I ...

Angular encountered an issue while attempting to differentiate '[object Object]'. The permissible types for differentiation are limited to arrays and iterables

I need help iterating through an Object received from a service call and displaying it in a table using *ngFor. Unfortunately, I encounter the following error during this process: Error trying to diff '[object Object]'. Only arrays and iterables ...

The 'items' property cannot be linked to 'virtual-scroller' as it is not a recognized attribute

I'm currently facing an issue with integrating virtual scroll into my Ionic 4 + Angular project. Previously, I relied on Ionic's implementation of virtual scroll (ion-virtual-scroll) which worked well initially. However, I encountered a major dr ...

Transmitting image data (blob) from the frontend to the backend using Next.js and tRPC (T3 stack)

I'm currently facing a challenge in sending a leaflet map image from the backend to the frontend using the leaflet-simple-map-screenshoter library for capturing the image. The library returns a blob, which I need to transmit back to the backend and sa ...