Description
π Search Terms
type propagation, type effects
β 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
I'd like to propose a new feature for TypeScript that I believe would significantly enhance the language's type safety for functions that invariably have side-effects.
Type Propagation is a feature that allows functions to modify the types of their arguments, at the call site, propagating these type changes throughout the codebase. This feature is inspired by TypeScript's existing type narrowing capabilities and concepts from languages like Rust, which enforce static guarantees about resource ownership and state transitions.
Type Propagation would enable functions to modify the types of their input parameters based on specified state transitions, providing a mechanism for enforcing type safety in functions with side effects.
Syntax:
function readStream(stream: Stream<'unread'>): void -> stream is Stream<'read'> {
// Implementation here
stream.state = 'read';
}
In this example, the function readStream
takes a Stream<'unread'>
as input and updates its state
property to 'read'
. The -> stream is Stream<'read'>
syntax indicates that after calling readStream
, the type of the stream
argument will be updated to Stream<'read'>
.
Type Propagation ensures that functions with side effects accurately reflect changes in the types of their arguments. This would prevent runtime errors, for example after reading a stream that can only be read once - the code can express type transformations in function signatures, making code more explicit, self-documenting, and hopefully easier to understand.
The feature is inspired by concepts found in languages like Rust, where functions can enforce static guarantees about resource ownership and state transitions.
Note that type propagations would need to propagate through multiple layers of function calls, ensuring that type modifications are applied consistently across the codebase. (For simplicity, I'm not covering this in detail now, since the proposal is at the "idea" stage - there would be more design work involved in defining this feature, including things like async/await, generics, and probably other things.)
π Motivating Example
// Define a type representing a stream with a mutable state property
type Stream<S extends 'unread' | 'read'> = {
state: S;
};
// Define a function that propagates type changes
function readStream(stream: Stream<'unread'>): void -> stream is Stream<'read'> {
stream.state = 'read';
}
// Usage example
const myStream: Stream<'unread'> = { state: 'unread' };
// Call readStream to transition myStream to a 'read' state
readStream(myStream);
// Now, myStream's type is Stream<'read'>
readStream(myStream); // π error
π» Use Cases
- What do you want to use this for? it could hopefully improve types of things like streams in Node and browser APIs - but it might also be useful for things like mutable state in UI libraries, type-safety in state management libraries, and so on.
- What shortcomings exist with current approaches? mainly, type narrowing can't account for effects.
- What workarounds are you using in the meantime? things like immutability, monads and functors might serve as workarounds, but can't account for existing APIs, or for types that are inherently mutable in nature, such as read-once streams.