Skip to content

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that `superjson` supports, but is faster and smaller.

License

Notifications You must be signed in to change notification settings

kiliman/remix-typedjson

Repository files navigation

remix-typedjson

All Contributors

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that superjson supports, but is faster and smaller.

NOTE: Although faster, remix-typedjson is nowhere near as flexible as superjson. It only supports a subset of types with no extensibility. If you need the advanced features of superjson, then I definitely recommend it.

Example site: https://remix-typedjson-example-production.up.railway.app/

Example repo: https://github.com/kiliman/remix-typedjson-example

The following types are supported:

  • Date
  • BigInt
  • Set
  • Map
  • RegExp
  • undefined
  • Error
  • NaN
  • Number.POSITIVE_INFINITY
  • Number.NEGATIVE_INFINITY

🚧 Work In Progress

Sets and Maps currently only support string keys and JSON serializable values. Complex types coming soon.

πŸ›  How to Use with Remix

In order to get full-type fidelity and type inference, you must be on Remix v1.6.5+. You will also need to import the following replacement functions.

typedjson

Installation

npm i remix-typedjson

Replacement for Remix json helper. It also supports the optional ResponseInit, so you can return headers, etc.

Make sure your loader and action use the new declaration format:

❌ export const loader: LoaderFunction = async ({request}) => {}
❌ export const action: ActionFunction = async ({request}) => {}

βœ… export const loader = async ({request}: LoaderFunctionArgs) => {}
βœ… export const action = async ({request}: ActionFunctionArgs) => {}

βœ… export async function loader({request}: LoaderFunctionArgs) {}
βœ… export async function action({request}: ActionFunctionArgs) {}

Usage

return typedjson(
  { greeting: 'hello', today: new Date() },
  // ResponseInit is optional, just like the `json` helper
  { headers: { 'set-header': await commitSession(session) } },
)

useTypedLoaderData

Replacement for Remix useLoaderData. Use the generic <typeof loader> to get the correct type inference.

Usage

const loaderData = useTypedLoaderData<typeof loader>()

useTypedActionData

Replacement for Remix useActionData. Use the generic <typeof action> to get the correct type inference.

Usage

const actionData = useTypedActionData<typeof action>()

typeddefer

✨ New in v0.3.0

Replacement for Remix defer helper. It also supports the optional ResponseInit, so you can return headers, etc.

Usage

return typeddefer({
  fastData: { message: 'This is fast data', today: new Date() },
  slowData: new Promise(resolve => setTimeout(resolve, 2000)).then(() => {
    return { message: 'This is slow data', tomorrow: new Date() }
  }),
})

<TypedAwait>

In your route component, use the new <TypedAwait> component instead of the Remix <Await> component

Usage

export default function DeferRoute() {
  const { fastData, slowData } = useTypedLoaderData<typeof loader>()
  return (
    <main>
      <h1>Defer Route</h1>
      <h2>Fast Data</h2>
      <pre>{JSON.stringify(fastData, null, 2)}</pre>
      <div>fastData.today is {fastData.today.toLocaleString()}</div>
      <Suspense fallback={<p>Loading slow data...</p>}>
        <TypedAwait
          resolve={slowData}
          errorElement={<p>Error loading slow data!</p>}
        >
          {slowData => (
            <div>
              <h2>Slow Data</h2>
              <pre>{JSON.stringify(slowData, null, 2)}</pre>
              <div>
                slowData.tomorrow is {slowData.tomorrow.toLocaleString()}
              </div>
            </div>
          )}
        </TypedAwait>
      </Suspense>
    </main>
  )
}

useTypedRouteLoaderData

Helper for useMatches that returns the route data based on provided route id

Usage

import { loader as rootLoader } from '~/root'

const rootData = useTypedRouteLoaderData<typeof rootLoader>('root')

useTypedFetcher

✨ Add support for key in v0.4.0

Replacement for Remix useFetcher. Use the generic <typeof loader|action> to get the correct type inference for the fetcher.data property.

Usage

const fetcher = useTypedFetcher<typeof action>({ key: 'abc' })
fetcher.data // data property is fully typed

redirect

In order to return a redirect, you will need to import the redirect function from this package, in order for the type inference to work properly.

However, you can also throw redirect() and you can use the original redirect function from Remix.

TypedMetaFunction

πŸ”₯ Removed in v0.4.0 since it didn't actually work correctly. Will be replaced with a typedmeta wrapper function in next release

You can now get typed arguments for both data and parentsData from your meta function export. Based on new feature coming to Remix

export const meta: TypedMetaFunction<typeof loader> = ({ data }) => {
  return {
    title: `Posts | ${data?.post.title}`,
  }
}
// for parentsData, you can specify a Record of typed loaders keyed by route id
// root.tsx
export type LoaderType = typeof loader
// routes/parent.tsx
export type LoaderType = typeof loader
// routes/child.tsx
import { type LoaderType as RootLoaderType } from '~/root'
import { type LoaderType as ParentLoaderType } from '~/routes/parent'

export const meta: TypedMetaFunction<
  typeof loader,
  // parent loader types keyed by route id
  {
    'root': RootLoader
    'routes/parent': ParentLoader
  }
> = ({ data, parentsData }) => {
  // access typed parent data by route id
  const rootData = parentsData['root']
  const parentData = parentsData['routes/parent']

  return {
    title: `Posts | ${data?.post.title}`,
  }
}

registerCustomType

✨ New in v0.2.0

remix-typed-json support a limited number of native types in order to keep the bundle small. However, if you need to support a custom type like Decimal, then use the registerCustomType API. This way you only pay the cost of the custom type if you use it.

type CustomTypeEntry<T> = {
  type: string
  is: (value: unknown) => boolean
  serialize: (value: T) => string
  deserialize: (value: string) => T
}

export function registerCustomType<T>(entry: CustomTypeEntry<T>)

Usage

Register the custom type in root.tsx once.

// root.tsx
import {
  typedjson,
  registerCustomType,
  useTypedLoaderData,
} from 'remix-typedjson'

import Decimal from 'decimal.js'

registerCustomType({
  type: 'decimal',
  is: (value: unknown) => value instanceof Decimal,
  serialize: (value: Decimal) => value.toString(),
  deserialize: (value: string) => new Decimal(value),
})

You can now serialize and deserialize the Decimal type.

// route.tsx
export function loader() {
  const d = new Decimal('1234567890123456789012345678901234567890')
  return typedjson({ greeting: 'Hello World', today: new Date(), d })
}

export default function Index() {
  const data = useTypedLoaderData<typeof loader>()

  return (
    <>
      <h2>Loader Data</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <ul>
        <li>today: {data.today.toLocaleString()}</li>
        <li>
          d instanceof Decimal: {data.d instanceof Decimal ? 'true' : 'false'}
        </li>
        <li>d: {data.d.toFixed(0)}</li>
      </ul>
    </>
  )
}

😍 Contributors

Thanks goes to these wonderful people (emoji key):

Kiliman
Kiliman

πŸ’» πŸ“–
Kent C. Dodds
Kent C. Dodds

πŸ’»
Simon Knott
Simon Knott

πŸ’» πŸ› ⚠️
Tony Truand
Tony Truand

πŸ’» ⚠️
Gregori Rivas
Gregori Rivas

πŸ’»
Afsah Nasir
Afsah Nasir

πŸ“–
Magnus Markling
Magnus Markling

πŸ’»
Jozsef Lazar
Jozsef Lazar

πŸ’»
Luke Bowerman
Luke Bowerman

πŸ’»
Dan Marshall
Dan Marshall

πŸ“–
Eric Allam
Eric Allam

πŸ“–

This project follows the all-contributors specification. Contributions of any kind welcome!

About

This package is a replacement for superjson to use in your Remix app. It handles a subset of types that `superjson` supports, but is faster and smaller.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published