Skip to main content
Version: Canary

Redwood Utility Types

Besides generating types for you, Redwood exposes a handful of utility types for Cells, Scenarios, and DbAuth. You'll see these helpers quite often if you use the generators, so let's walk through some of them. By the end of this, you'll likely see a pattern in these types and their use of Generics.

Cells​

Cells created using the generators come with all the types your normally need, including the CellSuccessProps, CellFailureProps, and CellLoadingProps utility types.

CellSuccessProps<TData, TVariables>​

This is used to type the props of your Cell's Success component. It takes two arguments as generics:

GenericDescription
TDataThe type of data you're expecting to receive (usually the type generated from the query)
TVariablesAn optional second parameter for the type of the query's variables

Not only does CellSuccessProps type the data returned from the query, but it also types the variables and methods returned by Apollo Client's useQuery hook!

web/src/components/BlogPost.cell.tsx
import type { FindBlogPostQuery, FindBlogPostQueryVariables } from 'types/graphql'

import type { CellSuccessProps } from '@redwoodjs/web'

// ...

type SuccessProps = CellSuccessProps<FindBlogPostQuery, FindBlogPostQueryVariables>

export const Success = ({
blogPost, // From the query. This is typed of course
refetch, // πŸ‘ˆ From Apollo Client. This is typed too!
fetchMore
}: SuccessProps) => {
// ...
}

CellFailureProps<TVariables>​

This gives you the types of the props in your Cell's Failure component. It takes TVariables as an optional generic parameter, which is useful if you want to print error messages like "Couldn't load data for ${variables.searchTerm}":

web/src/components/BlogPost.cell.tsx
import type { FindBlogPostQuery, FindBlogPostQueryVariables } from 'types/graphql'

import type { CellFailureProps } from '@redwoodjs/web'

// ...

export const Failure = ({
error,
variables // πŸ‘ˆ Variables is typed based on the generic
}: CellFailureProps<FindBlogPostQueryVariables>) => (
// ...
)

CellLoadingProps<TVariables>​

Similar to CellFailureProps, but for the props of your Cell's Loading component:

web/src/components/BlogPost.cell.tsx
import type { FindBlogPostQuery, FindBlogPostQueryVariables } from 'types/graphql'

import type { CellLoadingProps } from '@redwoodjs/web'

// ...

export const Loading = (props: CellLoadingProps<FindBlogPostQueryVariables>) => (
<div>Loading...</div>
)

Scenarios & Testing​

Over on the api side, when you generate SDLs and Services, Redwood generates tests and scenarios with all the types required. Let's take a deeper look at scenario types.

defineScenario​

This is actually a function, not a type, but it takes a lot of generics. Use as many or as few as you find helpful.

defineScenario<PrismaCreateType, TName, TKey>
GenericDescription
PrismaCreateType(Optional) the type imported from Prisma's create operation that goes into the "data" key
TName(Optional) the name or names of the models in your scenario
TKeys(Optional) the key(s) in your scenario. These are really only useful while you write out the scenario

An example:

posts.scenarios.ts
import type { Prisma, Post } from '@prisma/client'

export const standard = defineScenario<Prisma.PostCreateArgs, 'post', 'one'>({
//πŸ‘‡ TName
post: {
// πŸ‘‡ TKey
one: {
// πŸ‘‡ PrismaCreateType. Notice how we import the type from @prisma/client
data: { title: 'String', body: 'String', metadata: { foo: 'bar' } },
},
},
})

If you have more than one model in a single scenario, you can use unions:

defineScenario<Prisma.PostCreateArgs | Prisma.UserCreateArgs, 'post' | 'user'>

ScenarioData<TModel, TName, TKeys>​

This utility type makes it easy for you to access data created by your scenarios in your tests. It takes three generic parameters:

GenericDescription
TDataThe Prisma model that'll be returned
TName(Optional) the name of the model. ("post" in the example below)
TKeys(optional) the keys(s) used to define the scenario. ("one" in the example below)

We know this is a lot of generics, but that's so you get to choose how specific you want to be with the types!

api/src/services/posts/posts.scenario.ts
import type { Post } from '@prisma/client'

//...

export type StandardScenario = ScenarioData<Post, 'post'>
api/src/services/posts/posts.test.ts
import type { StandardScenario } from './posts.scenarios'

scenario('returns a single post', async (scenario: StandardScenario) => {
const result = await post({ id: scenario.post.one.id })
})

You can of course just define the type in the test file instead of importing it. Just be aware that if you change your scenario, you need to update the type in the test file too!

DbAuth​

When you setup dbAuth, the generated files in api/src/lib/auth.ts and api/src/functions/auth.ts have all the types you need. Let's break down some of the utility types.

DbAuthSession​

You'll notice an import at the top of api/src/lib/auth.ts:

api/src/lib/auth.ts
import type { DbAuthSession } from '@redwoodjs/api'

DbAuthSession is a utility type that's used to type the argument to getCurrentUser, session:

api/src/lib/auth.ts
export const getCurrentUser = async (session: DbAuthSession<number>) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true },
})
}

The generic it takes should be the type of your User model's id field. It's usually a string or a number, but it depends on how you've defined it.

Because a session only ever contains id, all we're doing here is defining the type of id.

DbAuthHandlerOptions​

DbAuthHandlerOptions gives you access to all the types you need to configure your dbAuth handler function in api/src/function/auth.ts. It also takes a generic, TUserβ€”the type of your User model. Note that this is not the same type as CurrentUser.

You can import the type of the User model directly from Prisma and pass it to DbAuthHandlerOptions:

import type { User as PrismaUser } from '@prisma/client'

import type { DbAuthHandlerOptions } from '@redwoodjs/api'

export const handler = async (
event: APIGatewayProxyEvent,
context: Context
) => {
// Pass in the generic to the type here πŸ‘‡
const forgotPasswordOptions: DbAuthHandlerOptions<PrismaUser>['forgotPassword'] = {

// ...

// Now in the handler function, `user` will be typed
handler: (user) => {
return user
},

// ...

}

// ...

}

Note that in strict mode, you'll likely see errors where the handlers expect "truthy" values. All you have to do is make sure you return a boolean. For example, return !!user instead of return user.

Directives​

ValidatorDirectiveFunc​

When you generate a validator directive you will see your validate function typed already with ValidatorDirectiveFunc<TDirectiveArgs>

import {
createValidatorDirective,
ValidatorDirectiveFunc,
} from '@redwoodjs/graphql-server'

export const schema = gql`
directive @myValidator on FIELD_DEFINITION
`
// πŸ‘‡ makes sure "context" and directive args are typed
const validate: ValidatorDirectiveFunc = ({ context, directiveArgs }) => {

This type takes a single generic - the type of your directiveArgs.

Let's take a look at the built-in @requireAuth(roles: ["ADMIN"]) directive, for example - which we ship with your Redwood app by default in ./api/src/directives/requireAuth/requireAuth.ts

type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>

const validate: RequireAuthValidate = ({ directiveArgs }) => {
// roles πŸ‘‡ will be typed correctly as string[] | undefined
const { roles } = directiveArgs
// ....
}
GenericDescription
TDirectiveArgsThe type of arguments passed to your directive in the SDL

TransformerDirectiveFunc​

When you generate a transformer directive you will see your transform function typed with TransformDirectiveFunc<TField, TDirectiveArgs>.

// πŸ‘‡ makes sure the functions' arguments are typed
const transform: TransformerDirectiveFunc = ({ context, resolvedValue }) => {

This type takes two generics - the type of the field you are transforming, and the type of your directiveArgs.

So for example, let's say you have a transformer directive @maskedEmail(permittedRoles: ['ADMIN']) that you apply to String fields. You would pass in the following types

type MaskedEmailTransform = TransformerDirectiveFunc<string, {permittedRoles?: string[]}>
GenericDescription
TFieldThis will type resolvedValue i.e. the type of the field you are transforming
TDirectiveArgsThe type of arguments passed to your directive in the SDL