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
:
{
"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, andundefined
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 null
s 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
:
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
},
}
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
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
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!
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]
) => {
// ...
}