Skip to content

generateClient / selectionSet types use NonNullable for belongsTo (and hasMany items), hiding null from TypeScript #3159

@RestingState

Description

@RestingState

Environment information

System:
  OS: Windows 11 10.0.26200
  CPU: (16) x64 13th Gen Intel(R) Core(TM) i7-13620H
  Memory: 2.05 GB / 15.73 GB
Binaries:
  Node: 22.22.0 - C:\Users\denys\AppData\Local\mise\installs\node\22.22.0\node.EXE
  Yarn: undefined - undefined
  npm: 10.9.4 - C:\Users\denys\AppData\Local\mise\installs\node\22.22.0\npm.CMD
  pnpm: undefined - undefined
NPM Packages:
  @aws-amplify/auth-construct: 1.11.2
  @aws-amplify/backend: 1.22.0
  @aws-amplify/backend-ai: Not Found
  @aws-amplify/backend-auth: 1.9.3
  @aws-amplify/backend-cli: 1.8.2
  @aws-amplify/backend-data: 1.6.4
  @aws-amplify/backend-function: 1.18.0
  @aws-amplify/backend-output-storage: 1.3.4
  @aws-amplify/backend-secret: 1.4.2
  @aws-amplify/backend-storage: 1.4.3
  @aws-amplify/cli-core: 2.2.4
  @aws-amplify/client-config: 1.10.1
  @aws-amplify/data-construct: 1.17.0
  @aws-amplify/data-schema: 1.22.2
  @aws-amplify/deployed-backend-client: 1.8.1
  @aws-amplify/form-generator: 1.2.6
  @aws-amplify/model-generator: 1.2.2
  @aws-amplify/platform-core: 1.11.0
  @aws-amplify/plugin-types: 1.12.0
  @aws-amplify/sandbox: 2.1.4
  @aws-amplify/schema-generator: 1.4.1
  @aws-cdk/toolkit-lib: 1.16.0
  aws-amplify: 6.16.3
  aws-cdk-lib: 2.235.0
  typescript: 6.0.2
No AWS environment variables
No CDK environment variables

Describe the bug

When using generateClient<Schema>() with a custom selectionSet on nested relations (e.g. User.get(..., { selectionSet: ["taskReservedAgents.task.*"] })), TypeScript infers nested belongsTo fields as non-nullable (e.g. Task), even though the GraphQL schema allows those fields to be null at runtime (orphaned join rows, missing parent, resolver behavior, etc.).

As a result, code such as taskReservedAgents.map((r) => r.task) is typed as Task[] instead of (Task | null)[], which is misleading and can hide real runtime cases where task is null.

This appears to come from ResolvedModel in @aws-amplify/data-schema-types: when unwrapping SingularReturnValue<infer M> and ListReturnValue<infer M>, the implementation uses NonNullable<M> before building the resolved shape used for selection-set inference. That strips nullability from relationship targets at the type level even when the API contract allows null.

Expected behavior: Nested relation fields that are nullable in GraphQL should be reflected in TypeScript as T | null (or optional) where appropriate, at least for belongsTo and list elements, so client code can safely narrow or filter.

Actual behavior: Types suggest a non-null related model where runtime may still return null.

Reproduction steps

  1. Define a join model with taskId and task: a.belongsTo("Task", "taskId") (and a User with taskReservedAgents: a.hasMany("TaskReservedAgent", "userId")).

  2. Call:

    const userWithTasks = await client.models.User.get(
      { id: userId },
      { selectionSet: ["taskReservedAgents.task.*"] },
    );
  3. Observe the inferred type of userWithTasks.data.taskReservedAgents[number].task (or after .map((r) => r.task)): it is Task, not Task | null.

  4. Contrast with GraphQL: the nested task field on TaskReservedAgent is typically nullable when the parent Task cannot be resolved.

Optional reference (library code): In @aws-amplify/data-schema-types, ResolvedModel applies NonNullable<M> for both ListReturnValue<infer M> and SingularReturnValue<infer M> branches when resolving relationship fields, which erases null from the inferred selection-set shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    pending-triageIncoming issues that need categorization

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions