Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nullish Coalescing and Logical Compound Assignments (??=, ||=, &&=) #37255

Closed
5 tasks done
danielrentz opened this issue Mar 6, 2020 · 14 comments · Fixed by #37727
Closed
5 tasks done

Nullish Coalescing and Logical Compound Assignments (??=, ||=, &&=) #37255

danielrentz opened this issue Mar 6, 2020 · 14 comments · Fixed by #37727
Labels
Committed The team has roadmapped this issue Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". ES Next New featurers for ECMAScript (a.k.a. ESNext) Help Wanted You can do this Suggestion An idea for TypeScript

Comments

@danielrentz
Copy link

Search Terms

Nullish coalescing assignment

Suggestion

A new operator ??= to assign some default to a variable/property if it is nullish.

It's also avaibale in PHP: https://wiki.php.net/rfc/null_coalesce_equal_operator

Examples

obj1.obj2.obj3.x ??= 42;
instead of
obj1.obj2.obj3.x = obj1.obj2.obj3.x ?? 42;

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@IllusionMH
Copy link
Contributor

This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)

Most likely this one should be unchecked, because this syntax is not supported by ES.

However there is proposal https://github.com/tc39/proposal-logical-assignment which is in Stage 2 and TS usually start implementation if proposal reaches Stage 3.
@DanielRosenwasser might provide more info about Stage 3 of that proposal :)

@danielrentz
Copy link
Author

danielrentz commented Mar 6, 2020

Hm negation confusion :) I checked this item because "This isn't a runtime feature" as in "This is a compile-time feature" because TSC needs to compile it to something else. Or what else is intended behind this item?

Thanks for the TC pointer. Makes sense to extend this to all logical operators!

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript Waiting for TC39 Unactionable until TC39 reaches some conclusion ES Next New featurers for ECMAScript (a.k.a. ESNext) labels Mar 6, 2020
@DanielRosenwasser
Copy link
Member

It's in a good direction in the standards pipeline. Iif I recall correctly, I was the only one who had some minor objections around tc39/proposal-logical-assignment#3 which I've changed my mind on. We'll look into implementing it when it hits stage 3.

@DanielRosenwasser DanielRosenwasser changed the title Nullish coalescing assignment (??=) Nullish Coalescing and Logical Compound Assignments (??=, ||=, &&=) Mar 6, 2020
@DanielRosenwasser DanielRosenwasser changed the title Nullish Coalescing and Logical Compound Assignments (??=, ||=, &&=) Nullish Coalescing and Logical Compound Assignments (??=, ||=, &&=) Mar 6, 2020
@Kingwl
Copy link
Contributor

Kingwl commented Mar 31, 2020

Could I take a try on this one after stage3?🙋🏻‍♂️

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Mar 31, 2020

It looks like it'll be discussed tomorrow. I've given my review approval and I can let you know when it moves forward. I've put together a write-up to consider for any implementations:


This new proposal adds a few new productions:

LeftHandSideExpression &&= AssignmentExpression

LeftHandSideExpression ||= AssignmentExpression

LeftHandSideExpression ??= AssignmentExpression

These roughly correspond to the following respectively:

LeftHandSideExpression && (LeftHandSideExpression = AssignmentExpression)

LeftHandSideExpression || (LeftHandSideExpression = AssignmentExpression)

LeftHandSideExpression ?? (LeftHandSideExpression = AssignmentExpression)

An implementation for TypeScript should at least test the following:

  • JavaScript emit where the LeftHandSideExpression is
    • an Identifier
    • a property access or element access where
      • the target is an Identifier (e.g. a.b, a["b"])
      • the target is not an Identifier (e.g. a.b.c, a.b["c"], foo().c) - to ensure that side effects don't get retriggered
    • a parenthesized LeftHandSideExpression
    • at least one example where AssignmentTargetType of the LeftHandSideExpression in the spec is not simple.
  • Control flow analysis within
    • the right side of each operator
      • (see &&= example below)
    • the code following each operator
      • function foo(result: number[] | undefined) {
            results ||= [];
            results.push(100);
        }
      • function foo(result: number[] | undefined) {
            results ??= [];
            results.push(100);
        }
      • interface ThingWithOriginal {
            name: string;
            original?: ThingWithOriginal
        }
        function doSomethingWithAlias(thing?: ThingWithOriginal | undefined) {
            if (thing &&= thing.original) {
                console.log(thing.name);
            }
        }
  • the evaluated type of each operator
    • function foo(result: number[] | undefined) {
          (results ||= []).push(100);
      }
    • function foo(result: number[] | undefined) {
          (results ??= []).push(100);
      }

@DanielRosenwasser
Copy link
Member

It just hit stage 3!

@DanielRosenwasser DanielRosenwasser added Committed The team has roadmapped this issue and removed Waiting for TC39 Unactionable until TC39 reaches some conclusion labels Mar 31, 2020
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 4.0 milestone Mar 31, 2020
@DanielRosenwasser DanielRosenwasser added Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". labels Mar 31, 2020
@Kingwl
Copy link
Contributor

Kingwl commented Apr 1, 2020

Yes, I saw your approve at tc39😳

@aminpaks
Copy link
Contributor

aminpaks commented Jun 16, 2020

Is this a bug or am I missing something?

let x: { a?: boolean } = {};

x.a ??= true;
x.a &&= false;
^^^ Type 'false' is not assignable to type 'true'.(2322)

Playground (Nightly build)

@Kingwl
Copy link
Contributor

Kingwl commented Jun 16, 2020

@aminpaks seems a bug. I'll take a look on it.

@kolpav
Copy link

kolpav commented Feb 16, 2021

@Kingwl is this bug too?

Playground

I don't see how props.size can be undefined

@danielrentz
Copy link
Author

danielrentz commented Feb 16, 2021

@kolpav I think this is unrelated but caused by the fact that narrowed types do not flow into callback functions.

You can workaround that by creating a local const size = prop.size ?? 'base'; and use that.

@kolpav
Copy link

kolpav commented Feb 16, 2021

@danielrentz Thats unfortunate. I would like to keep react props on props object because using your solution I need to have two ways how to access props based on them having default value or not. props.<propWithoutDefaultValue> and special variables (size etc) for every other prop. I know there are ways how to get around that but they are usually lot of code and not as readable and nice as props.size ??= 'base'. Is it something worth creating issue for or is it expected TS behaviour? My mental model must be wrong because I was bit surprised by it.

@gustavopch
Copy link

@kolpav I'm in the same situation. I don't see how props.size could be undefined. A workaround is to assign the default value using a function with an assertion signature:

type NonNullableProp<
  TObject extends { [key: string]: any },
  TKey extends keyof TObject
> = Omit<TObject, TKey> &
  {
    [key in TKey]-?: NonNullable<TObject[key]>
  }

function assignDefault<TProps>(
  props: TProps,
  key: keyof TProps,
  value: TProps[typeof key],
): asserts props is NonNullableKey<TProps, typeof key> {
  props[key] ??= value
}

const Component = (props: {
  size?: 'small' | 'medium' | 'large'
}) => {
  assignDefault(props, 'size', 'medium')
  // From this line, size is not considered optional anymore
  // ...
}

@kolpav
Copy link

kolpav commented Jun 27, 2021

@gustavopch Is exactly something I was trying to avoid

I know there are ways how to get around that but they are usually lot of code and not as readable and nice as props.size ??= 'base'

Your solution looks nice but in my case I am not willing to write code doing "nothing" just to please our type checking overlords. TS usually forces you to write better code but this time it doesn't seem to be the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Committed The team has roadmapped this issue Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". ES Next New featurers for ECMAScript (a.k.a. ESNext) Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants