Repository navigation
Self mutating typesΒ #47929
Description
Suggestion
π Search Terms
mutate generic, class method, type, changing, this type guard
β Viability Checklist
My suggestion meets these guidelines:
- 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 feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
Allow for mutating the this type inside a method for external users. Probably most notably useful in mutating the generics parameters of a certain class.
π Motivating Example
Fully typed arrays that mutate when using for example the .push method.
const array = [6, 'foo'] as [number, string];
array.push(99);
type NewArrayType = typeof array;
// We expect: [number, string, number]
// We get: [number, string]π» Use Cases
In my specific use-case, I have a process and all sort of different kinds of steps in that process. Data is of course transferred between those steps. Having steps declare what data they need and output is a bit verbose, but possible. Creating the process object that can (at runtime) decide how to combine the existing steps and have compile-time checks on if those steps will have the necessary data available is currently extremely convoluted.
/** A step simply converts the given data to something new */
type Step<TInput, TOutput> = (input: TInput) => Promise<TOutput>;
/** A process is simply an array of steps that can be executed all in order */
class Process<TData> {
public constructor(private initialData: TData) { }
/** Keep a list of all steps we have built up */
private steps: Step<any, any>[] = [];
/** Append a step to the process. Note that the return type is used for the workaround only. */
public push<TOutput>(step: Step<TData, TOutput>): this is Process<TOutput> {
this.steps.push(step);
return true;
}
/** Same as the above, but we'll return ourselves as a different type */
public push2<TOutput>(step: Step<TData, TOutput>): Process<TOutput> {
this.steps.push(step);
return this as unknown as Process<TOutput>; // TS doesn't like this change in type since TOutput has no relation to TInput
}
/** Execute all steps one by one, feeding the result of the previous one to the next */
public async run(): Promise<TData> {
let data = this.initialData;
for (const step of this.steps) {
data = await step(data);
}
return data;
}
}
// This step requires no data, and will create a property id
// Also, having TOutput be determined implicitely here from the lambda would be amazing
const setIdStep: Step<{}, { id: string }> = input => Promise.resolve({ id: '093c3d72-6b8b-413c-ad5e-8787226402c3' });
// This step requires an id and will create some data
const fetchDataStep: Step<{ id: string }, { id: string, data: { foo: number } }> = input => Promise.resolve({ ...input, data: { foo: 99 } });
// How we want to use this class without TS errors
async function desiredSyntax() {
const p = new Process({});
p.push(setIdStep);
p.push(fetchDataStep); //<- Error: Property 'id' is missing in type '{}' but required in type '{ id: string; }'.
const result = await p.run();
console.log(result.data.foo); //<- Error: Property 'data' does not exist on type 'Promise<{}>'.
}
// How to fool TS in thinking it is doing it correctly
async function workaround() {
const p = new Process({});
if (p.push(setIdStep)) {
if (p.push(fetchDataStep)) {
const result = await p.run(); // { id: string, data: any }
console.log(result.data.foo)
}
}
}
// Another workaround, but this gets even more error prone that the previous one
async function workaround2() {
const p = new Process({});
const p2 = p.push2(setIdStep); // Just assign a new variable which will have the correct type since we forced it to
const p3 = p2.push2(fetchDataStep);
const result = await p3.run();
console.log(result.data.foo);
}Note that both workaround methods work, but they add a lot of complexity to the code, are extremely error-prone and they do not properly reflect how it would be done in plain JS.
Additionally, this workaround forces us to have a fixed return type which is both too restrictive and unnecessary.
Activity
MartinJohns commentedon Feb 17, 2022
This sounds very much like #41339.
RyanCavanaugh commentedon Feb 17, 2022
I agree this is effectively session typing.
The entire sort of framework of this is problematic, since as soon as a mutating value gets aliased, the whole system becomes unsound because there's no reverse reference tracking going on (nor would we be inclined to add that) to invalidate those references.
didii commentedon Feb 17, 2022
Yes, that seems to be a close enough issue to mark this as a duplicate. I'll close this one off then, thanks.
Just for completeness sake, this is how my use-case could be implemented using the
assert this is <type>syntax as suggested in #41339.