-
-
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
.
@@ -18,7 +18,7 @@ import { | |||
// Details here: https://github.com/wasp-lang/wasp/issues/2017 | |||
export function makeQueryCacheKey<Input, Output>( | |||
query: Query<Input, Output>, | |||
payload: Input | |||
payload?: Input |
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.
payload !== undefined
hints at payload
being optional
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 this is equivalent to what we had. Perhaps Input
can be undefined and we only want this check to fire then (i.e., we never call this function without a payload).
Can you double check?
@@ -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'
@@ -93,4 +93,5 @@ type ClientOperationWithNonAnyInput<Input, Output> = | |||
? (args?: unknown) => Promise<Output> | |||
: [Input] extends [void] | |||
? () => Promise<Output> | |||
: (args: Input) => Promise<Output> | |||
// TODO: decide if this is what we want? | |||
: (args?: Input) => Promise<Output> |
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 get the error:
client/operations/hooks.ts(32,26): error TS2345: Argument of type 'Input | undefined' is not assignable to parameter of type 'Input'.
The useQuery
hook definition:
function useQuery<Input, Output>(
query: Query<Input, Output>,
queryFnArgs?: Input,
options?: any
): UseQueryResult<Output, Error>
has the queryFnArgs
as an optional argument: and it executes query(queryFnArgs)
, query
is:
query: Query<Input, Output>
which is in the end (args: Input) => Promise<Output>
.
This leads me to set the args
as optional here.
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.
@sodic alternativelly, I can just put a @ts-ignore
and not modify anything related to operations, so we can deal with this in the future?
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 don't fully understand what happened here, but our type tests should cover this. If you can change it without breaking them, then it should be fine.
That said, I'm 99% sure this change introduces a bug that the tests will catch. If they fail to catch it, then I'll look into it some more.
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.
// 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
Expect<Equal<typeof boolToStringAuth, (payload?: boolean) => Promise<string>>>, | ||
Expect<Equal<typeof boolToVoidNoAuth, (payload?: boolean) => Promise<void>>>, | ||
Expect<Equal<typeof boolToVoidAuth, (payload?: boolean) => Promise<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.
Yeah, this is what I suspected had to happen when I saw the change in the templates 😄
Anyway, we shouldn't introduce a regression in user space to make internal TS happy.
@@ -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.
@@ -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.
@@ -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.
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.