Skip to content

'identity' modifier to indicate a function's parameter-less returns should be narrowed like a valueΒ #60948

Open
@JoshuaKGoldberg

Description

@JoshuaKGoldberg

πŸ” Search Terms

indicate function returns identical value cached memoized signals

βœ… Viability Checklist

⭐ Suggestion

Many apps today are built on the concept of using small "getter" functions as wrappers around values. For example, Signals as implemented in Angular, Solid, and the stage 1 TC39 Signals proposal often look something like:

declare const value: () => string | undefined;

if (value() !== undefined) {
  console.log(value().toUpperCase());
}

Signals users have struggled with using them in TypeScript because, at present, there isn't a way to get that code block to type check without type errors. Signals users know that the result of value() must be string inside the if, but TypeScript doesn't have a way to note that the result should be type narrowed. Common workarounds today include !, ?., and refactoring to store intermediate values. All of which are at best unnecessary verbosity, and at worst conflict with frameworks.

Request: can we have a keyword -or, failing that, built-in / intrinsic type- to indicate that calls to a function produce a referentially equal, structurally unchanging value? In other words, that the function call (value()) should be treated by type narrowing as if it was just a variable reference (value)?

Proposal: how about an identity modifier keyword for function types that goes before the ()? It would be treated in syntax space similarly to other modifier keywords such as abstract and readonly.

πŸ“ƒ Motivating Example

When an identity function is called, it is given the same type narrowing as variables. Code like this would now type check without type errors, as if value was declared as const value: string | undefined:

declare const value: identity () => string | undefined;

if (value() !== undefined) {
  value();
  // Before: string | undefined
  // Now: string

  console.log(value().toUpperCase());
  // Before:  ~~~~~~~ Object is possibly 'undefined'.
  // Now: βœ…
}

Narrowing would be cleared the same as variables when, say, a new closure/scope can't be guaranteed to preserve narrowing:

declare const value: identity () => string | undefined;

if (value() !== undefined) {
  setTimeout(() => {
    value();
    // Still: string | undefined

    console.log(value().toUpperCase());
    // Still:   ~~~~~~~ Object is possibly 'undefined'.
  });
}

πŸ’» Use Cases

One difficult-to-answer design question is: how could identity handle functions with parameters? I propose the modifier not be allowed on function signaturess with parameters to start. It should produce a type error for now. The vast majority of Signals users wouldn't need signatures with parameters, so I don't think solidifying that needs to block this proposal. IMO that can always be worked on later.

Furthermore, it's common for frameworks to set up functions with a parameter-less "getter" signature and a single-parameter "setter" signature. I propose for an initial version of the feature, calling any other methods or setting to any properties on the type should clear type narrowing:

declare const value: {
  identity (): string | undefined;
  (newValue: string | undefined): void;
}

if (value() !== undefined) {
  value("...");

  value();
  // Still: string | undefined

  console.log(value().toUpperCase());
  // Still:   ~~~~~~~ Object is possibly 'undefined'.
}

More details on the difficulties of signals with TypeScript:

If a new modifier keyword isn't palatable, a fallback proposal could be a built-in type like Identity<T>. This wouldn't be a new utility type (FAQ: no new utility types); it'd be closer to the built-in template string manipulation types.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarified

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions