diff --git a/.changeset/old-actors-beg.md b/.changeset/old-actors-beg.md new file mode 100644 index 00000000000..b5d1bdf711c --- /dev/null +++ b/.changeset/old-actors-beg.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/nextjs': patch +'@clerk/types': patch +--- + +Add Autocomplete TS generic for union literals diff --git a/packages/clerk-js/src/core/resources/OrganizationMembership.ts b/packages/clerk-js/src/core/resources/OrganizationMembership.ts index 30366ade9a5..cc327fcd0f5 100644 --- a/packages/clerk-js/src/core/resources/OrganizationMembership.ts +++ b/packages/clerk-js/src/core/resources/OrganizationMembership.ts @@ -1,4 +1,5 @@ import type { + Autocomplete, ClerkPaginatedResponse, ClerkResourceReloadParams, GetUserOrganizationMembershipParams, @@ -20,9 +21,7 @@ export class OrganizationMembership extends BaseResource implements Organization /** * @experimental The property is experimental and subject to change in future releases. */ - // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string - // eslint-disable-next-line - permissions: (OrganizationPermission | (string & {}))[] = []; + permissions: Autocomplete[] = []; role!: MembershipRole; createdAt!: Date; updatedAt!: Date; @@ -41,8 +40,16 @@ export class OrganizationMembership extends BaseResource implements Organization search: convertPageToOffset({ ...retrieveMembershipsParams, paginated: true }) as any, }) .then(res => { + if (!res?.response) { + return { + total_count: 0, + data: [], + }; + } + + // TODO: Fix typing const { data: suggestions, total_count } = - res?.response as unknown as ClerkPaginatedResponse; + res.response as unknown as ClerkPaginatedResponse; return { total_count, @@ -99,9 +106,16 @@ export class OrganizationMembership extends BaseResource implements Organization }, { forceUpdateClient: true }, ); - const currentMembership = (json?.response as unknown as OrganizationMembershipJSON[]).find( + + if (!json?.response) { + return this.fromJSON(null); + } + + // TODO: Fix typing + const currentMembership = (json.response as unknown as OrganizationMembershipJSON[]).find( orgMem => orgMem.id === this.id, ); + return this.fromJSON(currentMembership as OrganizationMembershipJSON); } } diff --git a/packages/nextjs/src/server/authMiddleware.ts b/packages/nextjs/src/server/authMiddleware.ts index c6c675290d3..b07aece0cd6 100644 --- a/packages/nextjs/src/server/authMiddleware.ts +++ b/packages/nextjs/src/server/authMiddleware.ts @@ -1,6 +1,7 @@ import type { AuthObject, RequestState } from '@clerk/backend'; import { buildRequestUrl, constants } from '@clerk/backend'; import { isDevelopmentFromApiKey } from '@clerk/shared/keys'; +import type { Autocomplete } from '@clerk/types'; import type Link from 'next/link'; import type { NextFetchEvent, NextMiddleware, NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; @@ -32,15 +33,9 @@ type NextTypedRoute['0']['href']> = T extends string // For extra safety, we won't recommend using a `/(.*)` route matcher. type ExcludeRootPath = T extends '/' ? never : T; -// We want to show suggestions but also allow for free-text input -// the (string & {}) type prevents the TS compiler from merging the typed union with the string type -// https://github.com/Microsoft/TypeScript/issues/29729#issuecomment-505826972 -type RouteMatcherWithNextTypedRoutes = - | WithPathPatternWildcard> - | NextTypedRoute - // This is necessary to allow all string, using something other than `{}` here WILL break types! - // eslint-disable-next-line @typescript-eslint/ban-types - | (string & {}); +type RouteMatcherWithNextTypedRoutes = Autocomplete< + WithPathPatternWildcard> | NextTypedRoute +>; const INFINITE_REDIRECTION_LOOP_COOKIE = '__clerk_redirection_loop'; diff --git a/packages/types/src/clerk.retheme.ts b/packages/types/src/clerk.retheme.ts index c2a05cfdac2..862342be61a 100644 --- a/packages/types/src/clerk.retheme.ts +++ b/packages/types/src/clerk.retheme.ts @@ -18,7 +18,7 @@ import type { OrganizationResource } from './organization'; import type { MembershipRole } from './organizationMembership'; import type { ActiveSessionResource } from './session'; import type { UserResource } from './user'; -import type { DeepPartial, DeepSnakeToCamel } from './utils'; +import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; export type InstanceType = 'production' | 'development'; @@ -805,7 +805,7 @@ type PrimitiveKeys = { [K in keyof T]: T[K] extends string | boolean | number | null ? K : never; }[keyof T]; -type LooseExtractedParams = `:${T}` | (string & NonNullable); +type LooseExtractedParams = Autocomplete<`:${T}`>; export type OrganizationSwitcherProps = { /** diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index ba0e1b0077d..2d8fb3f691e 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -18,7 +18,7 @@ import type { OrganizationResource } from './organization'; import type { MembershipRole } from './organizationMembership'; import type { ActiveSessionResource } from './session'; import type { UserResource } from './user'; -import type { DeepPartial, DeepSnakeToCamel } from './utils'; +import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; export type InstanceType = 'production' | 'development'; @@ -805,7 +805,7 @@ type PrimitiveKeys = { [K in keyof T]: T[K] extends string | boolean | number | null ? K : never; }[keyof T]; -type LooseExtractedParams = `:${T}` | (string & NonNullable); +type LooseExtractedParams = Autocomplete<`:${T}`>; export type OrganizationSwitcherProps = { /** diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 29f3ddc0b53..1bce0fc2f10 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -17,7 +17,7 @@ import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './sig import type { OAuthStrategy } from './strategies'; import type { BoxShadow, Color, EmUnit, FontWeight, HexColor } from './theme'; import type { UserSettingsJSON } from './userSettings'; -import type { CamelToSnake } from './utils'; +import type { Autocomplete, CamelToSnake } from './utils'; import type { VerificationStatus } from './verification'; export interface ClerkResourceJSON { @@ -303,9 +303,7 @@ export interface OrganizationMembershipJSON extends ClerkResourceJSON { /** * @experimental The property is experimental and subject to change in future releases. */ - // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string - // eslint-disable-next-line - permissions: (OrganizationPermission | (string & {}))[]; + permissions: Autocomplete[]; public_metadata: OrganizationMembershipPublicMetadata; public_user_data: PublicUserDataJSON; role: MembershipRole; diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 71f4300e404..a772eb4b1c6 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -1,3 +1,5 @@ +import type { Autocomplete } from 'utils'; + import type { OrganizationResource } from './organization'; import type { ClerkResource } from './resource'; import type { PublicUserData } from './session'; @@ -28,9 +30,7 @@ export interface OrganizationMembershipResource extends ClerkResource { /** * @experimental The property is experimental and subject to change in future releases. */ - // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string - // eslint-disable-next-line - permissions: (OrganizationPermission | (string & {}))[]; + permissions: Autocomplete[]; publicMetadata: OrganizationMembershipPublicMetadata; publicUserData: PublicUserData; role: MembershipRole; @@ -40,9 +40,7 @@ export interface OrganizationMembershipResource extends ClerkResource { update: (updateParams: UpdateOrganizationMembershipParams) => Promise; } -// Adding (string & {}) allows for getting eslint autocomplete but also accepts any string -// eslint-disable-next-line -export type MembershipRole = 'admin' | 'basic_member' | 'guest_member' | (string & {}); +export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; export type OrganizationPermission = | 'org:sys_domains:manage' diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index b2cd63517cb..d7dd6dc9e9b 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,3 +1,5 @@ +import type { Autocomplete } from 'utils'; + import type { ActJWTClaim } from './jwt'; import type { OrganizationPermission } from './organizationMembership'; import type { ClerkResource } from './resource'; @@ -31,9 +33,7 @@ type CheckAuthorizationParams = } | { role?: never; - // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string - // eslint-disable-next-line - permission: OrganizationPermission | (string & {}); + permission: Autocomplete; } )[]; role?: never; @@ -47,9 +47,7 @@ type CheckAuthorizationParams = | { some?: never; role?: never; - // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string - // eslint-disable-next-line - permission: OrganizationPermission | (string & {}); + permission: Autocomplete; }; export interface SessionResource extends ClerkResource { diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index e3e28ea92b7..f2e344ecf69 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -83,3 +83,9 @@ type IsSerializable = T extends Function ? false : true; export type Serializable = { [K in keyof T as IsSerializable extends true ? K : never]: T[K]; }; + +/** + * Enables autocompletion for a union type, while keeping the ability to use any string + * or type of `T` + */ +export type Autocomplete = U | (T & Record);