Skip to content

Type Issues with resource's hasValue and defaultValue #63982

@rainerhahnekamp

Description

@rainerhahnekamp

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

Yes

Description

Description

When using resource with a defaultValue and guarding access via hasValue, the resource can become type never in subsequent else if branches on the same variable.

Without a defaultValue, the accompanying undefined in the value type prevents this: e.g. ResourceRef<number | undefined> narrows to ResourceRef<number> in the hasValue branch, but for the other branches remains ResourceRef<number | undefined>.

With a defaultValue, we start from ResourceRef<number>. hasValue stays with the same type, so another branch is impossible and narrows down to never. isLoading or error throws a compilation error.

Using a default value is quite common, especially with arrays ([]) or the null object pattern.

It is not really a bug per se, but not the best DX.

Minimal repro

import { resource } from '@angular/core';

describe('type inference resources', () => {
  const loader = () => Promise.resolve(1);
  const params = () => 1;

  describe('fails when default value is provided', () => {
    it('implicit type + default', () => {
      const n = resource({ loader, params, defaultValue: 0 });
      if (n.hasValue()) {
      } else if (n.isLoading()) { // TS error here when defaultValue is present
      } else if (n.error()) { // TS error here when defaultValue is present
      }
    });

    it('explicit type + default', () => {
      const n = resource<number, number>({ loader, params, defaultValue: 0 });
      if (n.hasValue()) {
      } else if (n.isLoading()) { // TS error here when defaultValue is present
      } else if (n.error()) { // TS error here when defaultValue is present
      }
    });
  });
});

Possible Solutions

If you see potential in one of these options, I’d be happy to create some prototypes (they could also serve as good first issues). My personal favourites are option 1 & 2, depending on how the implementation would look like.

  1. Keep the current approach
    Developers must remember that when dealing with resources that have default values, they cannot rely on hasValue. Instead, they should directly check the status property or use error/isLoading before calling hasValue. This limitation should be clearly documented.

  2. Introduce discriminated types for resource variants
    We could define distinct types for each resource state. Something along the lines of:

    // Pseudo-code to transport the idea
    
    type ResourceResolvedRef<T> = { status: signal('resolved'); value(): T };
    type ResourceLoadingRef     = { status: signal('loading') };
    type ResourceErrorRef       = { status: 'error'; error(): unknown };
    
    type ResourceRef<T> = ResourceResolvedRef<T> | ResourceLoadingRef | ResourceErrorRef;
  3. Refine hasValue typing
    Similar to option 2, but instead of full discriminators, we could adjust typing so that hasValue is not a type predicate when the resource’s type does not contain undefined. This might be achievable either with conditional types or by introducing additional resource interfaces, depending on which path proves simpler.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/github/rainerhahnekamp/bug-resource-has-value

Please provide the exception or error you saw

Typical TypeScript Compilation error for `never`

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 20.3.1
Node: 22.15.1
Package Manager: pnpm 10.11.0
OS: darwin arm64
    

Angular: 20.3.0
... common, compiler, compiler-cli, core, forms
... platform-browser, router

Package                      Version
------------------------------------
@angular-devkit/architect    0.2003.1
@angular-devkit/core         20.3.1
@angular-devkit/schematics   20.3.1
@angular/build               20.3.1
@angular/cli                 20.3.1
@schematics/angular          20.3.1
rxjs                         7.8.2
typescript                   5.9.2

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: reactivityWork related to fine-grained reactivity in the core frameworkcross-cutting: types

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions