-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enables strict null checks in SDK #2360
base: main
Are you sure you want to change the base?
Conversation
@@ -302,7 +302,7 @@ function AdditionalFormFields({ | |||
disabled={isLoading} | |||
/> | |||
{errors[field.name] && ( | |||
<FormError>{errors[field.name].message}</FormError> | |||
<FormError>{errors[field.name]!.message}</FormError> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why this is needed since we have the check errors[field.name] && (
?
@@ -69,7 +69,7 @@ async function getAuthUserData(userId: {= userEntityUpper =}['id']): Promise<Aut | |||
throwInvalidCredentialsError() | |||
} | |||
|
|||
return createAuthUserData(user); | |||
return createAuthUserData(user!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'm not sure why this is needed since we have the check if (!user) {
. Maybe it's not obvious to the compiler that the throwInvalidCredentialsError
fn throws?
const { data: task, isLoading } = tasksCrud.get.useQuery({ | ||
id: parseInt(id, 10), | ||
}); | ||
const { id } = useParams<{ id: string }>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not related to this PR, but a quick fix since id
is of type string | undefined
.
waspc/data/Generator/templates/sdk/wasp/client/operations/queries/core.ts
Outdated
Show resolved
Hide resolved
@@ -1,9 +1,9 @@ | |||
{{={= =}=}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've started to work on enabling strict null checks while working on env variables, so these are some changes I left from that effort - env
-> nodeEnv
since I imagined that the import of env vars would look like import { env } from 'wasp/server'
waspc/data/Generator/templates/sdk/wasp/client/operations/rpc.ts
Outdated
Show resolved
Hide resolved
18f9fd0
to
a509762
Compare
@@ -88,10 +88,10 @@ export function handleApiError(error: AxiosError<{ message?: string, data?: unkn | |||
// That would require copying HttpError code to web-app also and using it here. | |||
const responseJson = error.response?.data | |||
const responseStatusCode = error.response.status | |||
throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) | |||
return new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we change this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use handleApiError(error)
which throws the error in some fn - TS thinks that fn has undefined
as one of the return values.
If we do throw handleApiError(error)
in the same fn, TS now knows this throws and then the fn doesn't return i.e. it doesn't return undefined
.
export function throwInvalidCredentialsError(message?: string): void { | ||
throw new HttpError(401, 'Invalid credentials', { message }) | ||
export function createInvalidCredentialsError(message?: string): HttpError { | ||
return new HttpError(401, 'Invalid credentials', { message }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we change this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same reasoning as here: #2360 (comment)
@@ -6,37 +6,37 @@ const EMAIL_FIELD = 'email'; | |||
const TOKEN_FIELD = 'token'; | |||
|
|||
// PUBLIC API | |||
export function ensureValidEmail(args: unknown): void { | |||
export function ensureValidEmail<Args extends object>(args: Args): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the type parameter is unnecessary. It's enough to say:
ensureValidEmail(args: object) ...
See item 51 of Effective TypeScript for an explanation.
The same applies to all functions in the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Read Item 51
and it makes sense 👍
const identities = Object.values(data.identities).filter(Boolean); | ||
return identities.length > 0 ? identities[0].id : null; | ||
return identities.length > 0 ? identities[0]!.id : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should remove the assertion and change the filtering predicate:
const identities = Object.values(data.identities).filter(identity => identity !== null);
return identities.length > 0 ? identities[0].id : null;
Three benefits:
- TypeScript can now infer a type predicate so the
!
on the next line is no longer necessary. It's both safer and less fragile (the entire type procedure is localized and no longer spans two lines). identity => identity !== null
is cleaner thanBoolean
anyway. The first is clear to everyone, and the second only to those familiar with JS's quirks and idioms.
Note
Type predicate inference is a pretty new feature. It's available in TS 5.5, and our package.json
requires ^5.1.0
. This breaks the build for old users who have started their Wasp project before TS 5.5. was released.
We can either update TS to ^5.5
, or we can define an isNotNull
type guard as a named function and use that instead of a Boolean
. It behaves the same way but requires more ceremony.
The first option requires users to migrate stuff, while the second one doesn't. So it all depends on whether we want to release this as part of a minor or a major release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks the build for old users who have started their Wasp project before TS 5.5. was released.
Are you sure it breaks the build if we just bump the TS version in package.json
? Shouldn't it just install a newer TS version for them?
EDIT: the users have their TS version defined in their package.json
. Yep. But we could introduce validation for the typescript
package as well, so we could force them to bump it :)
But, I think the isNotNull
solution is more in the scope of this PR.
// TODO: figure out why overloading is not working | ||
// @ts-ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am strongly in favor of resolving all these issues now. It's unlikely we'll ever prioritize returning to them, and even if we did, we'd be missing context.
Figuring this stuff out is a part of this task and is a little annoying (that's mostly why I postponed it :)), but I believe it should be done.
I'd be more in favor of the "we'll figure it out later approach" if:
- The issue was about something else and these postponed fixes were tangential stuff.
- Merging this was a blocker for a critical bugfix.
But since figuring this stuff out is the central point of the issue, and since closing the issue isn't a blocker for a critical bugfix, I don't think postponing is justified.
To be clear, I'm not against banning ts-ignores altogether. But if we're using them, we should know why. Dirty hacks are sometimes ok, as long as we understand what we're doing and can document it.
Adding a hack we don't understand with a "figure this out later" todo is an extreme measure, and should be reserved reserved for production-is-down-and-were-losing-millions situations, with a clear plan on when to come back to it.
So, in short, I don't think we should add tsignores we don't understand (I may be convinced otherwise). We can meet in person and go through these issues together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed IRL: this PR has the goal of unblocking the #2362 PR and nothing will substantially change in the codebase - besides quick fixes and enabling the strict null checking option. It will have the effect that Zod works as expected and all future code has to be written with strict null checks. All the things that were "broken" until this point - will remain broken (in the eyes of Typescript).
I'd go through all the @ts-ignore
s and do one of the following:
- if we can fix it now - fix it
- if the fix requires going down a rabbit hole - make an issue which we will mention in a comment
- use type assertion instead of
@ts-ignore
if we can - makes the override more precise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We fixed this instance by provided an additional overload :)
@@ -29,7 +29,7 @@ export const onBeforeLogin: OnBeforeLoginHook = async ({ providerId }) => { | |||
|
|||
export const onAfterLogin: OnAfterLoginHook = async ({ prisma, user }) => { | |||
await prisma.user.update({ | |||
where: { id: user.id }, | |||
where: { id: user!.id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, how come we need this? Shouldn't the hook enforce the user code in its type signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, the hook should demand that user
exists and the end user shouldn't be exposed to this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now updated the types so user
can't be null
since we control the hook execution it makes sense to expose this kind of API to the end user.
@@ -73,7 +73,7 @@ export const toggleAllTasks: ToggleAllTasks = async (_args, context) => { | |||
|
|||
const whereIsDone = (isDone: boolean) => ({ | |||
isDone, | |||
user: { id: context.user.id }, | |||
user: { id: context.user!.id }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is surprising too. How come TS can't infer this from the check we did above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, I was surprised as well, but it wouldn't narrow the type for me - for some reason. We can talk about this IRL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trick is that this is happening in an arrow function - TS can't ensure that you checked that user
exists. I've now created an userId
variable outside of the arrow fn and it's okay.
@@ -100,7 +100,7 @@ type OnBeforeLoginHookParams = { | |||
/** | |||
* User that is trying to log in. | |||
*/ | |||
user: Awaited<ReturnType<typeof findAuthWithUserBy>>['user'] | |||
user: NonNullable<Awaited<ReturnType<typeof findAuthWithUserBy>>>['user'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, it's a little weird that this type is defined in terms of a function's return value. Also, how come the NonNullable
part is only now coming into the picture?
How come it's not defined using the User
type (and the function's return type would then also be defined using the User
type).
Have we discussed something like this already (defining values in terms of types and vice versa)? I'm having a Deja Vu but can't find it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a FindAuthWithUserResult
type that we didn't export - we should export it and use it instead of the return type 👍
NonNullable
came into play because findAuthWithUserBy
had the wrong return type of User
and not User | null
which is logical - user maybe doesn't exist in the DB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to use FindAuthWithUserResult
👍
username: process.env.SMTP_USERNAME!, | ||
password: process.env.SMTP_PASSWORD!, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is throwing all kinds of errors for me. I tried to build the server with TypeScript (npx tsc
in .wasp/out/server
) and it failed.
Does this work for you normally (I'm guessing it does, because you wouldn't know these types needed changing if it didn't).
I probably messed something up because it also fails on older versions of main. If everything's normal for you, I'll dig in.
This PR enables
strictNullChecks
option in the SDKtsconfig.json
.We want to enable this option to make sure Zod schemas are working properly before we start using them for env variables validation.
Left to do
@ts-ignore
and a TODO for things that need more workUsing
@ts-ignore
is not that problematic since it will unblock us for using Zod schemas, but also explicitly mark parts of the code base that need some work. This work was still needed before this PR, but it was implicit.