Skip to content

Commit 539fad7

Browse files
feat(clerk-js): Type errors.global as ClerkGlobalHookError (#7174)
Co-authored-by: Dylan Staley <88163+dstaley@users.noreply.github.com>
1 parent cc11472 commit 539fad7

File tree

8 files changed

+73
-36
lines changed

8 files changed

+73
-36
lines changed

.changeset/moody-parks-scream.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
---
5+
6+
[Experimental] Add types for errors used in new custom flow APIs

packages/clerk-js/src/core/events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ClerkError } from '@clerk/shared/error';
12
import { createEventBus } from '@clerk/shared/eventBus';
23
import type { TokenResource } from '@clerk/shared/types';
34

@@ -15,7 +16,7 @@ export const events = {
1516

1617
type TokenUpdatePayload = { token: TokenResource | null };
1718
export type ResourceUpdatePayload = { resource: BaseResource };
18-
export type ResourceErrorPayload = { resource: BaseResource; error: unknown };
19+
export type ResourceErrorPayload = { resource: BaseResource; error: ClerkError | null };
1920
export type ResourceFetchPayload = { resource: BaseResource; status: 'idle' | 'fetching' };
2021

2122
type InternalEvents = {

packages/clerk-js/src/core/signals.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isClerkAPIResponseError } from '@clerk/shared/error';
1+
import { type ClerkError, createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error';
22
import type { Errors, SignInSignal, SignUpSignal } from '@clerk/shared/types';
33
import { snakeToCamel } from '@clerk/shared/underscore';
44
import { computed, signal } from 'alien-signals';
@@ -7,7 +7,7 @@ import type { SignIn } from './resources/SignIn';
77
import type { SignUp } from './resources/SignUp';
88

99
export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resource: null });
10-
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });
10+
export const signInErrorSignal = signal<{ error: ClerkError | null }>({ error: null });
1111
export const signInFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' });
1212

1313
export const signInComputedSignal: SignInSignal = computed(() => {
@@ -21,7 +21,7 @@ export const signInComputedSignal: SignInSignal = computed(() => {
2121
});
2222

2323
export const signUpResourceSignal = signal<{ resource: SignUp | null }>({ resource: null });
24-
export const signUpErrorSignal = signal<{ error: unknown }>({ error: null });
24+
export const signUpErrorSignal = signal<{ error: ClerkError | null }>({ error: null });
2525
export const signUpFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' });
2626

2727
export const signUpComputedSignal: SignUpSignal = computed(() => {
@@ -38,7 +38,7 @@ export const signUpComputedSignal: SignUpSignal = computed(() => {
3838
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
3939
* generic non-API errors into the global array.
4040
*/
41-
function errorsToParsedErrors(error: unknown): Errors {
41+
function errorsToParsedErrors(error: ClerkError | null): Errors {
4242
const parsedErrors: Errors = {
4343
fields: {
4444
firstName: null,
@@ -62,29 +62,32 @@ function errorsToParsedErrors(error: unknown): Errors {
6262

6363
if (!isClerkAPIResponseError(error)) {
6464
parsedErrors.raw = [error];
65-
parsedErrors.global = [error];
65+
parsedErrors.global = [createClerkGlobalHookError(error)];
6666
return parsedErrors;
6767
}
6868

69-
error.errors.forEach(error => {
70-
if (parsedErrors.raw) {
71-
parsedErrors.raw.push(error);
72-
} else {
73-
parsedErrors.raw = [error];
74-
}
75-
76-
if ('meta' in error && error.meta && 'paramName' in error.meta) {
77-
const name = snakeToCamel(error.meta.paramName);
78-
parsedErrors.fields[name as keyof typeof parsedErrors.fields] = error;
79-
return;
80-
}
81-
82-
if (parsedErrors.global) {
83-
parsedErrors.global.push(error);
84-
} else {
85-
parsedErrors.global = [error];
86-
}
87-
});
69+
const hasFieldErrors = error.errors.some(error => 'meta' in error && error.meta && 'paramName' in error.meta);
70+
if (hasFieldErrors) {
71+
error.errors.forEach(error => {
72+
if (parsedErrors.raw) {
73+
parsedErrors.raw.push(error);
74+
} else {
75+
parsedErrors.raw = [error];
76+
}
77+
if ('meta' in error && error.meta && 'paramName' in error.meta) {
78+
const name = snakeToCamel(error.meta.paramName);
79+
parsedErrors.fields[name as keyof typeof parsedErrors.fields] = error;
80+
}
81+
// Note that this assumes a given ClerkAPIResponseError will only have either field errors or global errors, but
82+
// not both. If a global error is present, it will be discarded.
83+
});
84+
85+
return parsedErrors;
86+
}
87+
88+
// At this point, we know that `error` is a ClerkAPIResponseError and that it has no field errors.
89+
parsedErrors.raw = [error];
90+
parsedErrors.global = [createClerkGlobalHookError(error)];
8891

8992
return parsedErrors;
9093
}

packages/clerk-js/src/core/state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ClerkError } from '@clerk/shared/error';
12
import type { State as StateInterface } from '@clerk/shared/types';
23
import { computed, effect } from 'alien-signals';
34

@@ -36,7 +37,7 @@ export class State implements StateInterface {
3637
eventBus.on('resource:fetch', this.onResourceFetch);
3738
}
3839

39-
private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
40+
private onResourceError = (payload: { resource: BaseResource; error: ClerkError | null }) => {
4041
if (payload.resource instanceof SignIn) {
4142
this.signInErrorSignal({ error: payload.error });
4243
}

packages/shared/src/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { errorToJSON, parseError, parseErrors } from './errors/parseError';
22

33
export { ClerkAPIError } from './errors/clerkApiError';
44
export { ClerkAPIResponseError } from './errors/clerkApiResponseError';
5+
export { ClerkError } from './errors/clerkError';
56

67
export { buildErrorThrower, type ErrorThrower, type ErrorThrowerOptions } from './errors/errorThrower';
78

@@ -27,3 +28,5 @@ export {
2728
isUnauthorizedError,
2829
isUserLockedError,
2930
} from './errors/helpers';
31+
32+
export { createClerkGlobalHookError } from './errors/globalHookError';

packages/shared/src/errors/clerkApiError.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,7 @@ export class ClerkAPIError<Meta extends ClerkApiErrorMeta = any> implements Cler
1414
readonly meta: Meta;
1515

1616
constructor(json: ClerkAPIErrorJSON) {
17-
const parsedError = this.parseJsonError(json);
18-
this.code = parsedError.code;
19-
this.message = parsedError.message;
20-
this.longMessage = parsedError.longMessage;
21-
this.meta = parsedError.meta;
22-
}
23-
24-
private parseJsonError(json: ClerkAPIErrorJSON) {
25-
return {
17+
const parsedError = {
2618
code: json.code,
2719
message: json.message,
2820
longMessage: json.long_message,
@@ -36,6 +28,10 @@ export class ClerkAPIError<Meta extends ClerkApiErrorMeta = any> implements Cler
3628
isPlanUpgradePossible: json.meta?.is_plan_upgrade_possible,
3729
} as unknown as Meta,
3830
};
31+
this.code = parsedError.code;
32+
this.message = parsedError.message;
33+
this.longMessage = parsedError.longMessage;
34+
this.meta = parsedError.meta;
3935
}
4036
}
4137

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { isClerkApiResponseError } from './clerkApiResponseError';
2+
import type { ClerkError } from './clerkError';
3+
import { isClerkRuntimeError } from './clerkRuntimeError';
4+
5+
/**
6+
* Creates a ClerkGlobalHookError object from a ClerkError instance.
7+
* It's a wrapper for all the different instances of Clerk errors that can
8+
* be returned when using Clerk hooks.
9+
*/
10+
export function createClerkGlobalHookError(error: ClerkError) {
11+
const predicates = {
12+
isClerkApiResponseError,
13+
isClerkRuntimeError,
14+
} as const;
15+
16+
for (const [name, fn] of Object.entries(predicates)) {
17+
Object.assign(error, { [name]: fn });
18+
}
19+
20+
return error as ClerkError & typeof predicates;
21+
}
22+
23+
export type ClerkGlobalHookError = ReturnType<typeof createClerkGlobalHookError>;

packages/shared/src/types/state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ClerkGlobalHookError } from '../errors/globalHookError';
12
import type { SignInFutureResource } from './signInFuture';
23
import type { SignUpFutureResource } from './signUpFuture';
34

@@ -79,8 +80,9 @@ export interface Errors {
7980
raw: unknown[] | null;
8081
/**
8182
* Parsed errors that are not related to any specific field.
83+
* Does not include any errors that could be parsed as a field error
8284
*/
83-
global: unknown[] | null; // does not include any errors that could be parsed as a field error
85+
global: ClerkGlobalHookError[] | null;
8486
}
8587

8688
/**
@@ -143,6 +145,7 @@ export interface State {
143145
* An alias for `effect()` from `alien-signals`, which can be used to subscribe to changes from Signals.
144146
*
145147
* @see https://github.com/stackblitz/alien-signals#usage
148+
*
146149
* @experimental This experimental API is subject to change.
147150
*/
148151
__internal_effect: (callback: () => void) => () => void;
@@ -152,6 +155,7 @@ export interface State {
152155
* its dependencies change.
153156
*
154157
* @see https://github.com/stackblitz/alien-signals#usage
158+
*
155159
* @experimental This experimental API is subject to change.
156160
*/
157161
__internal_computed: <T>(getter: (previousValue?: T) => T) => () => T;

0 commit comments

Comments
 (0)