Skip to main content
Version: 3.x

TypeScript Strict Mode

Looks like you're ready to level up your TypeScript game! Redwood supports strict mode, but doesn't enable it by default. While strict mode gives you a lot more safety, it makes your code a bit more verbose and requires you to make small manual changes if you use the generators.

Enabling strict mode

Enable strict mode by setting strict to true in web/tsconfig.json and api/tsconfig.json, and if you're using scripts in scripts/tsconfig.json:

web/tsconfig.json, api/tsconfig.json, scripts/tsconfig.json
{
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"strict": true,
// ...
}
// ...
}

Redwood's type generator behaves a bit differently in strict mode, so now that you've opted in, make sure to generate types:

yarn rw g types

Manual tweaks to generated code

Now that you're in strict mode, there are some changes you need to make to get rid of those pesky red underlines!

null and undefined in Services

One of the challenges in the GraphQL-Prisma world is the difference in the way they treats optionals:

  • for GraphQL, optional fields can be null
  • but For Prisma, null is a value, and undefined means "do nothing"

This is covered in detail in Prisma's docs, which we strongly recommend reading. But the gist of it is that, for Prisma's create and update operations, you may have to make sure nulls are converted to undefined from your GraphQL mutation inputs. You'll have to think carefully about the behaviour you want - if the client is expected to send null, and you expect those fields to be set to null, you can make the field nullable in your Prisma schema. Sending a null will mean removing that value, sending undefined will mean that the field won't be updated.

For most cases however, you probably want to convert nulls to undefined - one way to do this is to use the removeNulls utility function from @redwoodjs/api:

api/src/services/users.ts
import { removeNulls } from "@redwoodjs/api"

export const updateUser: MutationResolvers["updateUser"] = ({ id, input }) => {
return db.user.update({
data: removeNulls(input),
where: { id },
})
}

Relation resolvers in services

Let's say you have a Post model in your schema.prisma that has an author field which is a relation to the Author model. It's a required field. This is what the Post model's SDL would probably look like:

export const schema = gql`
type Post {
id: Int!
title: String!
author: Author! # 👈 This is a relation; the `!` makes it a required field
authorId: Int!
# ...
}

When you generate SDLs or Services, the resolver for author is generated at the bottom of post.service.ts on the Post object. Because Post.author can't be null (we said it's required in the Prisma schema)—and because findUnique always returns a nullable value—in strict mode, you'll have to tweak this resolver:

// Option 1: Override the type
// The typecasting here is OK. `root` is the post that was _already found_
// by the `post` function in your Services, so `findUnique` will always find it!
export const Post: PostRelationResolvers = {
author: (_obj, { root }) =>
db.post.findUnique({ where: { id: root?.id } }).author() as Promise<Author>, // 👈
}

// Option 2: Check for null
export const Post: PostRelationResolvers = {
author: async (_obj, { root }) => {
// Here, `findUnique` can return `null`, so we have to handle it:
const maybeAuthor = await db.post
.findUnique({ where: { id: root?.id } })
.author()

if (!maybeAuthor) {
throw new Error('Could not resolve author')
}

return maybeAuthor
},
}
An optimization tip

If the relation truly is required, it may make more sense to include author in your post Service's Prisma query and modify the Post.author resolver accordingly:

export const post: QueryResolvers['post'] = ({ id }) => {
return db.post.findUnique({
include: {
author: true,
},
where: { id },
})
}

export const Post: PostRelationResolvers = {
author: async (_obj, { root }) => {
if (root.author) {
return root.author
}

const maybeAuthor = await db.post.findUnique(// ...

This will also help Prisma make a more optimized query to the database, since every time a field on Post is requested, the post's author is too! The tradeoff here is that any query to Post (even if the author isn't requested) will mean an unnecessary database query to include the author.

Roles checks for CurrentUser in src/lib/auth

When you setup auth, Redwood includes some template code for handling roles with the hasRole function. While Redwood does runtime checks to make sure it doesn't access roles if it doesn't exist, TypeScript in strict mode will highlight errors, depending on whether you are returning roles, and whether those roles are string or string[]

export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}

const currentUserRoles = context.currentUser?.roles
// Error: Property 'roles' does not exist on type '{ id: number; }'.ts(2339)

You'll have to adjust the generated code depending on your User model.

Example code diffs

A. If your project does not use roles

If your getCurrentUser doesn't return roles, and you don't use this functionality, you can safely remove the hasRole function.

B. Roles on current user is a string

Alternatively, if you define the roles as a string, you can remove the code that does checks against Arrays

api/src/lib/auth.ts
export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}

const currentUserRoles = context.currentUser?.roles

if (typeof roles === 'string') {
- if (typeof currentUserRoles === 'string') {
return currentUserRoles === roles
- }
}

if (Array.isArray(roles)) {
- if (Array.isArray(currentUserRoles)) {
- return currentUserRoles?.some((allowedRole) =>
- roles.includes(allowedRole)
- )
- } else if (typeof currentUserRoles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some((allowedRole) => currentUserRoles === allowedRole)
- }
}

// roles not found
return false
}

C. Roles on current user is an Array of strings

If in your User model, roles are an array of strings, and can never be just a string, you can safely remove most of the code

api/src/lib/auth.ts
export const hasRole = (roles: AllowedRoles): boolean => {
if (!isAuthenticated()) {
return false
}

const currentUserRoles = context.currentUser?.roles

if (typeof roles === 'string') {
- if (typeof currentUserRoles === 'string') {
- return currentUserRoles === roles
- } else if (Array.isArray(currentUserRoles)) {
// roles to check is a string, currentUser.roles is an array
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
- }
}

if (Array.isArray(roles)) {
- if (Array.isArray(currentUserRoles)) {
return currentUserRoles?.some((allowedRole) =>
roles.includes(allowedRole)
)
- } else if (typeof currentUserRoles === 'string') {
- return roles.some(
- (allowedRole) => currentUserRoles === allowedRole
- )
}
}

// roles not found
return false
}

getCurrentUser in api/src/lib/auth.ts

Depending on your auth provider—i.e., anything but dbAuth—because it could change based on your account settings (if you include roles or other metadata), we can't know the shape of your decoded token at setup time. So you'll have to make sure that the getCurrentUser function is typed.

To help you get started, the comments above the getCurrentUser function describe its parameters' types. We recommend typing decoded without using imported types from Redwood, as this may be a little too generic!

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

// Example 1: typing directly
export const getCurrentUser: CurrentUserFunc = async (
decoded: { id: string, name: string },
{ token, type }: { token: string, type: string },
) => {
// ...
}

// Example 2: Using AuthContextPayload
export const getCurrentUser: CurrentUserFunc = async (
decoded: { id: string, name: string },
{ token, type }: AuthContextPayload[1],
{ event, context }: AuthContextPayload[2]
) => {
// ...
}