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!
Service functions & tests
By default, Redwood's generators assume that Service functions don't require any parameters. For example, in the tutorial, the posts
Service function generated by the scaffold command doesn't specify any parameters:
export const posts: QueryResolvers['posts'] = () => {
return db.post.findMany()
}
While this is true, in the strictest sense, all GraphQL resolvers (and most of your Service functions do end up becoming GraphQL resolvers) take some sort of input, so you'll need to modify the calls to your Service functions that have been typed with QueryResolvers
or MutationResolvers
:
describe('posts', () => {
scenario('returns all posts', async (scenario: StandardScenario) => {
const result = await posts() // 🛑 error
const result = await posts({}) // ✅
expect(result.length).toEqual(Object.keys(scenario.post).length)
})
})
In strict mode, Redwood generates different types for QueryResolvers
and MutationResolvers
, where the first argument—args
—is no longer optional.
We realize that this isn't a perfect solution, but the tension comes from the fact that we can't define different types for when a Service function is called from inside GraphQL (as a resolver) versus when it's called from anywhere else.
Returning Prisma's findUnique
operation in Services
In strict mode, TypeScript becomes a lot more pedantic about null checks. One place where you'll encounter this in particular is when returning Prisma's findUnique
operation in Service functions.
The tension here is that Prisma returns promises in the form of Promise<Model | null>
, but the resolver types expect it in the form of Promise<Model> | Promise<null>
. At runtime, this has no effect. But the TS compiler needs to be told that it's okay:
export const post: QueryResolvers['post'] = ({ id }) => {
return db.post.findUnique({
where: { id },
}) as Promise<Post> | Promise<null>
}
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 have to make sure null
s are converted to undefined
from your GraphQL mutation inputs.
One way to do this is to use the dnull package:
yarn workspace api add dnull
import { dnull } from "dnull"
export const updateUser: MutationResolvers["updateUser"] = ({ id, input }) => {
return db.user.update({
data: dnull(input),
where: { id },
})
}
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 context?.currentUser?.roles === 'string') {
// roles to check is an array, currentUser.roles is a string
return roles.some(
(allowedRole) => context.currentUser?.roles === 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]
) => {
// ...
}