Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add strong typing for JSON.stringify #124

Closed
wants to merge 25 commits into from
Closed

✨ Add strong typing for JSON.stringify #124

wants to merge 25 commits into from

Conversation

aaditmshah
Copy link

@aaditmshah aaditmshah commented Mar 25, 2023

This PR builds on top of #123. Here's a comparison of the two branches.

Related pull requests

How do I use the replacer function?

The biggest change made in this PR is providing a sound type for JSON.stringify when the replacer function is provided. You can now use generics to specify what the input and return types of the replacer function should be.

Best Practice: Always provide the generic type to JSON.stringify if you're using the replacer function. If you don't do so, then TypeScript will use the incorrect type definition of JSON.stringify from the built-in library.

So, how do you use the new and improved replacer function? Consider the following example.

declare const importantDates: Map<string, Date>;

type Value = Date | Map<string, Date>;

// The type of `result` is just `string` because the `replacer` function never returns `undefined`.
const result = JSON.stringify<Value>(importantDates, (key, value) =>
  // The type of `value` is `string | Map<string, Date>`.
  typeof value === "string" ? value : Object.fromEntries(value)
);

There are a lot of things going on here.

  1. We provided the input generic type Date | Map<string, Date> instead of just Map<string, Date>. This is because the replacer processes the input value recursively. Hence, the first time when the replacer is called with Map<string, Date>, it converts the input map into an object. Subsequently, the replacer is called on the values of that object which are of type Date. Hence, we need to provide the input generic type Date | Map<string, Date>.

    If we provide just Map<string, Date> as the input generic type then we'll get an error.

    declare const importantDates: Map<string, Date>;
    
    type Value = Map<string, Date>;
    
    const result = JSON.stringify<Value>(importantDates, (key, value) =>
      Object.fromEntries(value) // TypeError: Type `Date` is not assignable to type `Map<string, Date>`
    );
  2. Notice that even though the input generic type is Date | Map<string, Date>, yet the type of value inside the replacer function is string | Map<string, Date>. This is because Date objects in JavaScript have a toJSON property which returns a string, and JSON.stringify calls the toJSON method to change the Date to a string before calling the replacer function.

    The new and improved type definitions for JSON.stringify are aware of this fact and provide the correct type.

  3. Notice that the return type is just string instead of string | undefined. This is because the replacer function in the above example never returns undefined. Hence, JSON.stringify can never return undefined.

    On the flip side, we could return undefined from the replacer function to filter out certain values. However, if we do so then the return type would be string | undefined. Consider the following example.

    declare const importantDates: Map<string, Date>;
    
    type Value = Date | Map<string, Date>;
    
    // The type of `result` is `string | undefined` because the `replacer` returns `undefined`.
    const result = JSON.stringify<Value>(importantDates, (key, value) => {
      if (typeof value !== "string") return Object.fromEntries(value);
      // Filter out all dates before the 20th century.
      return new Date(value) < new Date("1900-01-01") ? undefined : value;
    });

Conclusion

As you can see, we can give very strong type definitions for JSON.stringify without compromising on type-safety and soundness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant