question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Represent the types of function parameters that mutate inside the function.

See original GitHub issue

In JavaScript is possible to mutate objects inside functions. Right now, the following code in JavaScript:

function merge(x,y) {
  Object.assign(x,y);
} 

let x = {a: 1};   
merge(x, {b: 2});
console.log(x.b);

Can’t be written in TypeScript without casting the type. There are a few options whose type definition is wrong in all scenarios I can think of (maybe I’m missing a better option):

Option 1

let x: {a: number, b: number} = {a: 1}; // Error, missing b
merge(x, {b: 2});

Option 2

let x: {a: number, b: number} = {a: 1, b: 2};
merge(x, {b: null});
// From here, x.b is not a number anymore, but you could do
let y: number= x.b;

Suggestion

There could be an extension to function parameter definition like the following:

// then keyword indicates that before it can be type A, and after it will be of type A&B.
function merge<A,B>(x: A then x2: A&B, y: B) {
  Object.assign(x2,y);
}

let x: {a: number, b: number} = {a: 1, b: 2};
merge(x, {b: null});
// Here, type of x is {a: number, b: number} & {b: null}
x.b; // Type null

There, we indicate that whatever type was x before, now it is something different. The code above could be written in TypeScript as follows:

// then keyword indicates that before it can be type A, and after it will be of type A&B.
function merge<A,B>(x: A, y: B) {
  Object.assign(x,y);
}

let x: {a: number, b: number} = {a: 1, b: 2};
merge(x, {b: null});
// Here, type of x is {a: number, b: number} & {b: null}
let xAfterMerge = x as {a: number, b: number} & {b: null};
// Since this line, x should not be used but xAfterMerge
xAfterMerge.b; // Type null

Another example

interface Before {
  address: string;
}

interface After {
  addr: string;
}

function map(userb: Before then usera: After) {
   usera.addr = userb.address;
   delete userb.address;
}

let u = {adress: "my street"};
map(u);
console.log(u.addr);

That could be syntax sugar for this:

interface Before {
  address: string;
}

interface After {
  addr: string;
}

function map(userb: any) {
   userb.addr = userb.address;
   delete userb.address;
}

let u = {adress: "my street"};
map(u); 
console.log((u as After).addr);

Syntax

It could be something like:

identifier: type *then* identifier: type  

With the identifiers being different, and with the types being mandatory an extension of Object.

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:15
  • Comments:8 (2 by maintainers)

github_iconTop GitHub Comments

5reactions
jo3-lcommented, Nov 10, 2020

I can’t comment on the feasibility of this idea but I know I have wanted it before. I like creating type-safe builder style interfaces in typescript but currently the only way I know of to avoid losing the types is to make sure you always chain the builder calls and if you need to store the intermediate stages of a builder, always ensure it is stored in a fresh variable. Example:

interface Builder<Props = {}> {
  addProp<T extends string, V>(prop: T, val: V): Builder<Props & Record<T, V>>
  finish(): Props
}
declare function newBuilder(): Builder

const builderA = newBuilder()
builderA.addProp("a", "mystring")
builderA.addProp("b", 123)
const resultA = builderA.finish() // unfortunately resultA has type {}

let builderB = newBuilder()
builderB = builderB.addProp("a", "mystring")
builderB = builderB.addProp("b", 123)
const resultB = builderB.finish() // resultB also has type {}

const builderC1 = newBuilder()
const builderC2 = builderC1.addProp("a", "mystring")
const builderC3 = builderC2.addProp("b", 123)
const resultC = builderC3.finish() // Now has type {a: string, b: number} as desired

const resultD = newBuilder()
  .addProp("a", "mystring")
  .addProp("b", 123)
  .finish() // also has type {a: string, b: number} as desired

In a perfect world it’d be nice if there was some way to model stateful changes like builderA. I mostly get around it by making the builders immutable so you have to chain or reassign the variable to use it at all, however the builderB case still causes problems with the types even then.

Just encountered this issue as well (typing stateful builders), rather disappointed to find out that it’s not really possible to do so without chaining the calls together. Does seem unlikely that this will be supported in TypeScript anytime soon as the original issue mutating function parameters is mostly covered by the asserts keyword. In case anyone’s searching for this, here’s the original code used as an example in this issue rewritten to type-check correctly:

function merge<T>(x: unknown, y: T): asserts x is (typeof x) & T {
  Object.assign(x, y);
}

const x = { a: 1 };   
merge(x, { b: 2 });
// No error now!
console.log(x.b);
3reactions
Olian04commented, Aug 5, 2018

Another way of implementing this could be by adding a keyword that suggests that the function will modify the type of a parameter. We could borrow the out keyword from C#.

function Special(out obj) {
  obj['foo'] = () => {};
}

const bar = {};
Special(out bar);
bar.foo(); // ts-ok, js-ok

In the function scope an out argument should be considered a const value but not an immutable value.

function Fail(out obj) {
  obj = {}; // ts-error assigning to const
}

let obj = {};
Fail(out obj);

The out keyword is NOT optional.

function Fail(out obj) {}

let obj = {};
Fail(obj); // ts-error overload error, no overload takes a single "none out" argument

We could then explicitly type the out argument with the previously purposed then keyword.

function Special<T>(out obj: T then T & { foo(): void } ) {
  obj['foo'] = () => {};
}

const bar = {};
Special(out bar);
bar.foo(); // ts-ok, js-ok

I would just like to mention that you can achieve something close to this by “abusing” the typegurad feature.

function Special<T>(obj: T): obj is T & { foo(): void; } {
  obj['foo'] = () => {};
  return true;
}

const bar = {};
if (!Special(bar)) throw 'Will never happen';
bar.foo(); // ts-ok, js-ok
Read more comments on GitHub >

github_iconTop Results From Across the Web

Correct Style for Python functions that mutate the argument
The first way: def change(array): array.append(4) change(array). is the most idiomatic way to do it. Generally, in python, we expect a ...
Read more >
Functions - JavaScript - MDN Web Docs
Values can be passed to a function as parameters, and the function will return a value. In JavaScript, functions are first-class objects, ...
Read more >
Mutate Function in R Programming - Video & Lesson Transcript
The mutate function takes as parameters the data set, the new variable name you will create, and the modification/mutation. Maybe you're just ...
Read more >
6.1 mutate() | R for Graduate Students - Bookdown
This is a beginner's guide to coding in R.
Read more >
Learning JavaScript functions and how you should use them
Function names and parameters are both considered variable names in JavaScript. ... to understand what we mean when we talk about reassignment and...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found