Description
π Search Terms
indicate function returns identical value cached memoized signals
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β 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:
- Angular: signals: TypeScript and nullabilityΒ angular/angular#49161
- Solid: Signals are troublesome with TypescriptΒ solidjs/solid#1575
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.