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
-
Define a join model with taskId and task: a.belongsTo("Task", "taskId") (and a User with taskReservedAgents: a.hasMany("TaskReservedAgent", "userId")).
-
Call:
const userWithTasks = await client.models.User.get(
{ id: userId },
{ selectionSet: ["taskReservedAgents.task.*"] },
);
-
Observe the inferred type of userWithTasks.data.taskReservedAgents[number].task (or after .map((r) => r.task)): it is Task, not Task | null.
-
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.
Environment information
Describe the bug
When using
generateClient<Schema>()with a customselectionSeton nested relations (e.g.User.get(..., { selectionSet: ["taskReservedAgents.task.*"] })), TypeScript infers nestedbelongsTofields as non-nullable (e.g.Task), even though the GraphQL schema allows those fields to benullat runtime (orphaned join rows, missing parent, resolver behavior, etc.).As a result, code such as
taskReservedAgents.map((r) => r.task)is typed asTask[]instead of(Task | null)[], which is misleading and can hide real runtime cases wheretaskisnull.This appears to come from
ResolvedModelin@aws-amplify/data-schema-types: when unwrappingSingularReturnValue<infer M>andListReturnValue<infer M>, the implementation usesNonNullable<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 forbelongsToand 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
Define a join model with
taskIdandtask: a.belongsTo("Task", "taskId")(and aUserwithtaskReservedAgents: a.hasMany("TaskReservedAgent", "userId")).Call:
Observe the inferred type of
userWithTasks.data.taskReservedAgents[number].task(or after.map((r) => r.task)): it isTask, notTask | null.Contrast with GraphQL: the nested
taskfield onTaskReservedAgentis typically nullable when the parentTaskcannot be resolved.Optional reference (library code): In
@aws-amplify/data-schema-types,ResolvedModelappliesNonNullable<M>for bothListReturnValue<infer M>andSingularReturnValue<infer M>branches when resolving relationship fields, which erases null from the inferred selection-set shape.