Coder Social home page Coder Social logo

kiliman / remix-typedjson Goto Github PK

View Code? Open in Web Editor NEW
348.0 3.0 18.0 345 KB

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: MIT License

TypeScript 100.00%

remix-typedjson's Introduction

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 loader = async ({request}: LoaderArgs) => {}
โœ… export async function loader({request}: LoaderArgs) {}

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>()

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

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>()
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

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>) {
  customTypeMap.set(entry.type, entry)
}

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!

remix-typedjson's People

Contributors

danmarshall avatar ericallam avatar glomyst avatar joelazar avatar kentcdodds avatar kiliman avatar lukebowerman avatar memark avatar prodbygr avatar skn0tt avatar tony-truand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

remix-typedjson's Issues

Cannot read properties of undefined when properties with a `.` in the name clash.

To start, I just want to say thanks for this project and the work that's gone into it. It's been really helpful to us.

Example reproduction here: https://github.com/baker-travis/typedjson-repro/blob/main/app/routes/index.tsx#L8

In that example, you can see that firstObject is a property on the root object, but so is firstObject.deeper. deeper is not a property on firstObject. Because of the way the meta is transferred with dot notation, when it tries to deserialize firstObject.deeper.anotherProperty, it assumes deeper is on the firstObject property, whereas the truth is that firstObject.deeper is its own property. The result is that useTypedLoaderData throws an error saying TypeError: Cannot read properties of undefined (reading 'anotherProperty').

The meta object that contains the info needed to add richer types back upon deserialization looks like this:

{
  "firstObject.deeper.anotherProperty": "date"
}

which doesn't give it enough info to actually know that firstObject.deeper needs to be accessed as a whole, and not as separate entities.

I'll leave my suggestion here. Something like this makes a lot of sense to me to transport that meta:

[
  {
    path: ["firstObject.deeper", "anotherProperty"],
    type: "date"
  }
]

It's more verbose, but depends on the actual keys instead of doing your own concatenation and parsing.

Required `TypedJsonResponse` to be exported

Hello,

I am using an ESlint rule that requires me to explicitly set my function return types. So I need to type the return of my loader function.

Example:

const loader: ({ request, params }: LoaderArgs) => Promise<TypedJsonResponse<LoaderData>>

It doesn't seem that TypedJsonResponse is importable from this library making it not possible to explicitly set the return type.

Error on remix 1.7.0

Upon installing remix 1.7.0 I started having this issue with remix-typedjson:

Error: You must render this element in a remix route element
[dev:*server]     at invariant (/Users/.../app/node_modules/@remix-run/react/dist/invariant.js:15:11)
[dev:*server]     at useRemixRouteContext (/Users/.../app/node_modules/@remix-run/react/dist/components.js:179:3)
[dev:*server]     at useLoaderData (/Users/.../app/node_modules/@remix-run/react/dist/components.js:964:10)
[dev:*server]     at useTypedLoaderData (/Users/.../app/node_modules/remix-typedjson/dist/remix.js:20:44)
[dev:*server]     at App (/Users/.../app/src/app/root.tsx:58:29)
[dev:*server]     at renderWithHooks (/Users/.../app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5662:16)
[dev:*server]     at renderIndeterminateComponent (/Users/.../app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5735:15)
[dev:*server]     at renderElement (/Users/.../app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5950:7)
[dev:*server]     at renderNodeDestructiveImpl (/Users/.../app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6108:11)
[dev:*server]     at renderNodeDestructive (/Users/.../app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6080:14)

Support Decimal type serialization

For those of us using Prisma and the Decimal type, the current result of a serialized value is coming across as an object and not sticking with the true decimal.js type. ๐Ÿ˜„

`useTypedFetcher`'s `data` can be `undefined`

Maybe I got something wrong, but the data property returned by useTypedFetcher can be undefined. TS does not complain, while the runtime code fails.

const { data }  = useTypedFetcher<typeof loader>;
console.log(data.foo); // error thrown, Cannot read properties of undefined etc.

Taking a look at the implementation:

export type TypedFetcherWithComponents<T> = Omit<FetcherWithComponents<T>, 'data'> & {
    data: UseDataFunctionReturn<T>; 

// ==> should be 

export type TypedFetcherWithComponents<T> = Omit<FetcherWithComponents<T>, 'data'> & {
    data: UseDataFunctionReturn<T> | undefined; // add undefined here
};

Workaround

import type { FetcherWithComponents } from "@remix-run/react";
import type { UseDataFunctionReturn } from "remix-typedjson";
import { useTypedFetcher as useTypedFetcherOriginal } from "remix-typedjson";

export function useTypedFetcher<T>(): Omit<FetcherWithComponents<T>, "data"> & {
  data: UseDataFunctionReturn<T> | undefined;
} {
  return useTypedFetcherOriginal<T>();
}

`typedDefer`?

Now that Remix is React-Router'ed, I think a wrapper around defer is needed. A useTypedAsyncValue counterpart may be needed as well.

`useTypedLoaderData` Syntax Error during another Error

Trying to reproduce with a minimal example:

export const loader = async ({
    request,
  }: LoaderArgs): Promise<TypedJsonResponse<LoaderData>> => {
  
    const data = {
      date: new Date()
    };
  
    return typedjson(data);
};


export default function Dashboard(): JSX.Element {
  const loaderData = useTypedLoaderData<typeof loader>();
  const { date } = loaderData;
  // Purposefully throw an error to show issue
  throw new Error("TEST")
  return null;
}

Error message in browser console window:

Uncaught SyntaxError: Unexpected identifier 'Nov'

When checking the file its talking about in Sources tab
You'll see something like this in the file:

'routeData':{'routes/dashboard': {'date':Thu Nov 03 2022 11:04:25 GMT-0400 (Eastern Daylight Time)}}'

Seems like the typedjson is causing syntax errors somewhere due to the type conversions?

useTypedRouteLoaderData() type inference not working

On my root.tsx:

type LoaderData = {
	isLoggedIn: boolean
}

export const loader = async ({ request }: LoaderArgs): Promise<TypedJsonResponse<LoaderData>> => typedjson({ isLoggedIn: await isLoggedIn(request) })

On my navigation:

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

export const Navigation = () => {
	const { isLoggedIn } = useTypedRouteLoaderData<typeof rootLoader>('root')
	        ^^^^^^^^^^ Property 'isLoggedIn' does not exist on type 'LoaderData | undefined'.
	return <p>This is my header</p>
}

Am I doing something wrong? ๐Ÿค”

`useMatches` type inference

I found myself wanting to access a loader response generated by remix-typedjson via useMatches and ran into difficulty trying to specify the type of the data attribute.

I tried doing various incantations of:

match as Awaited<Promise<ReturnType<typeof loader>>>

In so doing I was never able to get the typedJson unwrapped from the response type, the closest I managed to get was:

Response & { typedjson(): Promise<{ foo: string, bar: number }> }

When Remix 1.10 lands useRouteLoaderData (https://reactrouter.com/en/main/hooks/use-route-loader-data) seems like it'll offer a better model for accessing loader data from elsewhere in the tree much more elegantly than the useMatches route so perhaps I'm barking up the wrong tree entirely.

`useTypedFetcher` make TS complain about incompatible types

I use the packages all over the place in my codebase and today I wanted to take advantage of typed fetcher. I use Remix Validated Form for all forms. It gives a possibility to pass a fetcher instance to a form. With the default useFetcher from Remix there is no problem but as soon as I change it to useTypedFetcher Typescript starts to complain about incompatibilities between types.

const fetcher = useTypedFetcher<SearchResponse | ErrorResponse>();
// with standard useFetcher there's no problem
// const fetcher = useFetcher<SearchResponse | ErrorResponse>(); 

<ValidatedForm
  action={"/api/search"}
  fetcher={fetcher}
  noValidate
  validator={validator}
  >
...
</ValidatedForm>

Error message:

Type 'TypedFetcherWithComponents<SearchResponse | ErrorResponse>' is not assignable to type 'FetcherWithComponents<any> | undefined'.
  Type 'TypedFetcherWithComponents<SearchResponse | ErrorResponse>' is not assignable to type '{ state: "idle"; type: "done"; formMethod: undefined; formAction: undefined; formData: undefined; formEncType: undefined; submission: undefined; data: any; } & { Form: ForwardRefExoticComponent<FormProps & RefAttributes<...>>; submit: SubmitFunction; load: (href: string) => void; }'.
    Type 'TypedFetcherWithComponents<SearchResponse | ErrorResponse>' is not assignable to type '{ state: "idle"; type: "done"; formMethod: undefined; formAction: undefined; formData: undefined; formEncType: undefined; submission: undefined; data: any; }'.
      Types of property 'state' are incompatible.
        Type '"idle" | "loading" | "submitting"' is not assignable to type '"idle"'.
          Type '"loading"' is not assignable to type '"idle"'.ts(2322)
index.d.ts(184, 5): The expected type comes from property 'fetcher' which is declared here on type 'IntrinsicAttributes & { validator: Validator<{ q?: string | undefined; }>; onSubmit?: ((data: { q?: string | undefined; }, event: FormEvent<HTMLFormElement>) => void | Promise<...>) | undefined; ... 5 more ...; disableFocusOnError?: boolean | undefined; } & Omit<...>'

It's unclear for me if this is something I can/should fix on my own or is related to Remix Validated Form or rather to remix-typedjson?

Add type definition for the new V2_Meta Remix API

Hi,
Love the project! Thanks for working on this.

I just started implementing some of the new Remix APIs that are coming in 2.0 and typescript is throwing errors with the TypedMetaFunction as it's using the old V1_HtmlMetaDescriptor return type.

Any chance to add V2_TypedMetaFunction to fix that?

Thanks!

Is it possible to have data parameter typed in meta function export?

Firstly, thank you for the awesome library and for bringing end-to-end type safety to Remix.

I have a problem that is specific to the exported meta function from a route.

Have a posts/$slug route which has a loader which exports few posts and rendered markdown in html format. With remix-typedjson it looks something like this:

export const loader = async ({ params }: LoaderArgs) => {
  invariant(params.slug, "params.slug is required")

  const post = await getPost(params.slug)

  invariant(post, `Post not found: ${params.slug}`)

  const html = marked(post.markdown)

  return typedjson({
    post,
    html,
  })
}

But when I want to access the returned data in the meta function to add a title for my page, I obviously don't get the types as MetaFunction by default has data set to any:

export const meta: MetaFunction = ({ data }) => {
  console.log("Data:", data)

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

Is it possible to type data returned from loader in the meta function?

Could we make this lib work with Cloudflare Workers platform?

When I use remix-typedjson with Remix + Cloudflare Workers template, there is the following error:

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
TypeError: Cannot read properties of null (reading 'useContext')

Error: Cannot find module 'tslib'

I am testing a deployment to fly.io and am getting an error:

[info] > start
[info] > cross-env NODE_ENV=production node ./build/server.js
[info] Error: Cannot find module 'tslib'
[info] Require stack:
[info] - /myapp/node_modules/remix-typedjson/dist/remix.js
[info] - /myapp/node_modules/remix-typedjson/dist/index.js
[info] - /myapp/build/index.js
[info] - /myapp/build/server.js
[info] at Function.Module._resolveFilename (node:internal/modules/cjs/loader:956:15)
[info] at Function.Module._load (node:internal/modules/cjs/loader:804:27)
[info] at Module.require (node:internal/modules/cjs/loader:1028:19)
[info] at require (node:internal/modules/cjs/helpers:102:18)
[info] at Object.<anonymous> (/myapp/node_modules/remix-typedjson/dist/remix.js:4:17)
[info] at Module._compile (node:internal/modules/cjs/loader:1126:14)
[info] at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
[info] at Module.load (node:internal/modules/cjs/loader:1004:32)
[info] at Function.Module._load (node:internal/modules/cjs/loader:839:12)
[info] at Module.require (node:internal/modules/cjs/loader:1028:19)

I noticed your tsconfig.json has importHelpers set to true.

"importHelpers": true

Are consumers responsible for also installing tslib, or is there a better way to resolve this error?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.