Skip to content

Support an incremental adoption of Fragment Masking #9075

Open
@charpeni

Description

@charpeni

Is your feature request related to a problem? Please describe.

I recently migrated a project to client-preset (from gql-tag-operations-preset) and would like to turn on Fragment Masking for it. Unfortunately, because of existing fragments that are not using Fragment Masking, we already have a thousand errors to handle to be able to.

When you enable Fragment Masking, all fragments, and operations are automatically masked, even though not all fragments or operations use Fragment Masking.

I would like the ability to unmask fragments recursively to support an incremental adoption of Fragment Masking.

Also related to:

Nested Fragment: <UserAvatar>

export const UserAvatarUserFragment = gql(`
  fragment UserAvatarUser on User {
    avatarUrl
  }
`);

type UserAvatarProps = {
  user: ResultOf<typeof UserAvatarUserFragment>;
};

function UserAvatar(props: UserAvatarProps) {
  return <img src={props.user.avatarUrl} />;
}

Fragment: <UserCard>

export const UserCardUserFragment = gql(`
  fragment UserCardUser on User {
    id
    name
    ...UserAvatarUserFragment
  }
`);

type UserCardProps = {
  user: ResultOf<typeof UserCardUserFragment>;
};

function UserCard(props: UserCardProps) {
  return (
    <>
      <UserAvatar user={props.user} />
      {/*         ^ Property 'avatarUrl' is missing in type  */}
      {/*         ^ Type '"UserCardUserFragment"' is not assignable to type '"UserAvatarUserFragment"' */}
      <div>ID: {props.user.id}</div>
      <div>Name: {props.user.name}</div>
    </>
  );
}

Query: <ExampleComponent>

export const ExampleComponentQuery = gql(`
  query ExampleComponent($id: String!) {
    user(id: $id) {
      ...UserCardUser
    }
  }
`);

type ExampleComponentProps = {
  id: VariablesOf<typeof ExampleComponentQuery>['id'];
};

export function ExampleComponent(props: ExampleComponentProps) {
  const { loading, data } = useQuery(ExampleComponentQuery, { variables: { id: props.id } });

  if (loading) {
    return <>loading...</>;
  }

  if (!data?.user) {
    return <>no user</>;
  }

  return <UserCard user={data.user} />;
}

Describe the solution you'd like

We could expose the following utility type: UnmaskResultOf<TypedDocumentNode>.

This would check if whether we're inside an operation or directly within a fragment and then recursively flatten fragments by resolving $fragmentRefs and merging them to the root fragment.

type UserCardProps = {
- user: ResultOf<typeof UserCardUserFragment>;
- // ^? (property) user: { __typename: "User"; id: string; name: string; ' $fragmentRefs': { ... }}
+ user: UnmaskResultOf<typeof UserCardUserFragment>;
+ // ^? (property) user: { __typename: "User"; id: string; name: string; avatarUrl: string }
};
const testUnmaskResultOf: UnmaskResultOf<ExampleComponentQuery> = {
  __typename: 'Query',
  user: {
    __typename: 'User',
    id: 'some-id',
    name: 'some-name',
    avatarUrl: 'some-avatar-url',
  },
};

See the implementation on TypeScript Playground: Link.

Warning: Pull request with the full implementation will follow.

Describe alternatives you've considered

No response

Is your feature request related to a problem? Please describe.

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions