Open4

Ruby でお馴染みの演算子 ||= が ES2021 に搭載されていたから使用感レビュー

rena-hrena-h

Ruby の自己代入演算式 ||=

ruby を書く人にはお馴染みのこれ ||=

ES2021 から新しく追加された 代入演算子 (Assignment Operators) : ||=, &&=, ??= は、Ruby にインスパイアされたものらしいです。
TC39 での proposal issue

モチベーション

このようなコードを書くことがありますよね?

function example(opts) {
  // Ok, but could trigger setter.
  opts.foo = opts.foo ?? 'bar'

  // No setter, but 'feels wrong' to write.
  opts.baz ?? (opts.baz = 'qux');
}

example({ foo: 'foo' })

これを新しい演算子 ??= を使って書いてみると、このようにスッキリ書くことができます!

function example(opts) {
  // Setters are not needlessly called.
  opts.foo ??= 'bar'

  // No repetition of `opts.baz`.
  opts.baz ??= 'qux';
}

example({ foo: 'foo' })

代入演算子 (Assignment Operators) : ||=, &&=, ??=

2021年6月にES2021 が出た
その時に追加された ||=, &&=, ??=

対応 node version 15以上

https://node.green/#ES2021-features-Logical-Assignment

TS 4.0 から

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#short-circuiting-assignment-operators

仕様書

https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-assignment-operators

rena-hrena-h

論理和代入 (||=)

論理和代入演算子 (x ||= y) は、x が偽値である場合にのみ代入を行います。

この x ||= y という式は、以下のように分解できます。
x = x || y

const user = { name: 'オナマエ', age: 0, message: '' };

user.name ||= 'ワーイ( ^ω^ )ワーイ';
console.log(user.name);
// output: 'オナマエ'

user.age ||= 20;
console.log(user.age);
// output: 20

user.message ||= 'ハロハロ~( ^ω^ )';
console.log(user.message);
// output: 'ハロハロ~( ^ω^ )'

0 , '' のように 偽値 (false) と判別されるものに代入されているのがわかります。

論理積代入 (&&=)

論理和代入 (||=) の逆版

論理積代入 (x &&= y) 演算子は、x が真値である場合にのみ代入を行います。

const user = { name: 'オナマエ', age: 0, message: '' };

user.name &&= 'ワーイ( ^ω^ )ワーイ';
console.log(user.name);
// output: 'ワーイ( ^ω^ )ワーイ'

user.age &&= 20;
console.log(user.age);
// output: 0

user.message &&= 'ハロハロ~( ^ω^ )';
console.log(user.message);
// output: ''

真値 (true) と判別されるものに代入されているのがわかります

Null 合体代入 (??=)

Null 合体代入 (x ??= y) 演算子は、x が nullish (null または undefined) である場合にのみ代入を行います。

user オブジェクト に存在しない property に対して、 ??= で代入することができる。

const user = { name: 'オナマエ' , age: null};

user.age ??= 20;
user.message ??= 'メッセージだよ'

console.log(user);
// output: { "name": "オナマエ", "age": 20, "message": "メッセージだよ"}  
// null だった age プロパティの値が更新され、存在しなかった message プロパティが入ってる!

??=||=

??|| の違いと同じ。
|| は 0 や '' を falsely と判別してしまうので、x ||= y だと適さない場合がある

let user1 = {name: null, message: undefined, age: 0}
let user2 = {name: null, message: undefined, age: 0}

user1.name ||= 'name'
user1.message ||= 'message'
user1.address ||= 'address'
user1.age ||= 20

user2.name ??= 'name'
user2.message ??= 'message'
user1.address ??= 'address'
user2.age ??= 20

console.log(user1) 
// { "name": "name", "message": "message", "address": "address", "age": 20 }
// null や undefined プロパティの値が更新され、存在しなかった address プロパティが入ってる!
// age の 0 は偽値(falsey)のため、 20 に更新されている

console.log(user2) 
// { "name": "name", "message": "message", "address": "address", "age": 0 }
// null や undefined プロパティの値が更新され、存在しなかった address プロパティが入ってる!
// age の値は null or undefined ではないため、 age: 0 のまま
rena-hrena-h

false に変換されうる式の例

  • null
  • NaN
  • 0
  • 空文字列 ("" または '' または ``)
  • undefined

Null 合体 (??)

左辺が null または undefined の場合に右の値を返し、それ以外の場合に左の値を返します。

console.log(undefined ?? '??'); // "??"
console.log(null ?? '??'); // "??"
console.log('' ?? '??'); // ''
console.log(false ?? true); // false
console.log(0 ?? 1); // 0

論理和 (||)

x || y は、x が true に変換できる場合は x を返し、それ以外の場合は y を返します。
= x が偽値である場合は y を返す

console.log(undefined || '||')  // "||"
console.log(null || '||')  // "||"
console.log('' || '||')  // "||"
console.log(false || true)  // true
console.log(0 || 1); // 1
rena-hrena-h

user のデータを更新する関数があるとする

if文を使う書き方

async updateUser(user: User, name: string | undefined, age: string | undefined): Promise<User> {
    if (name) {
        user.name = name;
    }
    if (age) {
        user.age = age;
    }
    // user を save する処理
}

論理和 || を使う書き方

引数 name, age は string | undefined としたとき

async updateUser(user: User, name: string | undefined, age: string | undefined): Promise<User> {
    user.name = name || user.name;  // name が undefined のとき user.name に user.nameを代入されたい
    user.age = age || user.age;  // age が undefined のとき user.age に user.ageを代入されたい
    // user を save する処理
}

しかしこれだと、少し問題がありそう。
|| では、name, age が null or undefined 以外の 0 や '' でも falsely になってしまう。

  • 引数の name が '' のとき、user.name には user.name が代入される
  • 引数の age が 0 のとき、user.age には user.age が代入される

=> 引数が null or undefined の時のみ、判別したい

Null 合体 ?? を使う書き方

??では、左辺が null or undefined のときのみ、falsely と判別されるため、

  • 引数の name が '' のとき、user.name には name が代入される
  • 引数の age が 0 のとき、user.age には age が代入される
async updateUser(user: User, name: string | undefined, age: string | undefined): Promise<User> {
    user.name = name ?? user.name;  // name が undefined のとき user.name に user.nameを代入されたい
    user.age = age ?? user.age;  // age が undefined のとき user.age に user.ageを代入されたい
    // user を save する処理
}