iTranslated by AI
(Revised) Is the Ternary Operator Really Unreadable? The Benefits of the Postfix "else if" Method in TypeScript

This article is a revised version of this one. I have also improved the code.
Is the ternary operator really hard to read?
Making code as readable as a document provides many benefits. Furthermore, some syntaxes in programming languages exist solely to improve readability. The ternary operator is one of them.
const value = x === 1 ? "A" : "B";
I'm sure many of you think, "No way, the ternary operator is hard to read." However, in my experience, there are cases where it is readable. You might have experienced this too. I researched why the readability varies and identified the key points. However, no language specification currently supports them. Therefore, I have developed a method to write readable ternary expressions in TypeScript.
TypeScript code using this method looks like this. It is similar to Python's conditional expressions (ternary operator), but slightly different.
const message =_( `名前: ${name}` ).elseIf(!name).then("");
That's all it does. It's very simple. However, if you intuitively feel that this way of writing is good, you can start using it immediately by just copying the class definition at the end. There is no need to read a long explanation.
Without using a ternary expression, the code would look like this. I would like to compare not only the amount of code and its appearance but also which one is easier to understand and why, while looking at practical samples.
if (!name) {
var message = "";
} else {
var message = `名前: ${name}`;
}
❗Note: I am not saying that you should replace all ternary operators or conditional branches with the postfix else if method introduced in this article. Please use it only in the cases where it improves readability as explained in the text.
Python's conditional expressions (ternary operator) were supposed to be designed for readability, but...
First, let's talk about the readability and unreadability of the ternary operator.
The ternary operator in languages like TypeScript, Java, and C looks like this:
const value = x == 1 ? "A" : "B";
It is quite startling to see == immediately following =, isn't it? (Although seeing => after = is also startling). Often, it takes a moment to process that it's a ternary operator after seeing the ?. While wrapping the condition in parentheses might make it less startling, the dissonance of encountering a condition when expecting a value still remains.
const value = (x == 1) ? ("A") : ("B");
Why does it take time to process? Because we don't encounter it often. Symbols and abbreviations are effective when they are used frequently; they are unsuitable as expressions if they are rarely read. Even with X (formerly Twitter), which we see every day, it is hard to grasp what "X" is pointing to. The justification that one should use abbreviations for local variables because their definitions are nearby is also inappropriate. A tendency to want to create rules is a bad habit of intermediate programmers. Consequently, some coding rules state that ternary operators should be avoided.
Python's conditional expression (ternary operator) can be considered a language specification built with readability in mind, based on the unreadability of C-style ternary operators. It is nice to see a philosophy that moves away from symbols.
value = "A" if x == 1 else "B"
For people who read English on a daily basis, having if on the right is easier to read because it aligns with English grammar. For clarity, this code executes the exact same logic as the following conditional block:
if x == 1:
value = "A"
else:
value = "B"
Why put if on the right? It is said to be to avoid making the reader process unimportant, exceptional information first. For instance, consider the following sentence:
However, you will be ineligible unless you spend at least 1,000 yen. Otherwise, you will receive points equivalent to 10% of your purchase!
Starting with "However," you wonder, "What are we talking about all of a sudden!?" You understand that points are involved, but the main subject is obscured. Even though it is just a change in order, the sentence seems to lose its focus. To write readable code, you must avoid leading with unimportant, exceptional details. This has a massive impact, so it is worth being meticulous about it.
Actually, Python's conditional expressions are hard to read
For the reasons mentioned above, one might say that Python's conditional expressions (ternary operator) are a readable way of expression. In reality, however, they are not that easy to read.
value = "A" if x == 1 else "B"
While they might be readable for an average person who reads English daily, what programmers read daily is programming languages—specifically, conditional blocks written in the order of Condition A, Value A, Value B. They are not English sentences written in the order of Value A, Condition A, Value B. Have you ever misread a Python conditional expression as Value A, Condition B, Value B? This is especially likely for Japanese speakers. I think it happens to non-Japanese speakers too, as they also accept object-oriented styles that are close to natural language. Note that in TypeScript, a conditional block looks like this (and x === 1 is not a mistake here):
if (x === 1) {
var value = "A";
} else {
var value = "B";
}
As a side note, in TypeScript, replacing the var above with let or const would result in a syntax error. Do you dislike var? Even if you were to declare it with let at the top and initialize it with a dummy value, that's just code with no reading value and only increases waste.
💡
varcannot "statically" detect variable name collisions, but the more you have to read, the higher the chance of making mistakes. If variables are named appropriately, collisions occur in less than 1% of cases. Also, in Visual Studio Code, clicking a variable name highlights other instances, making it easy to notice if you've accidentally used the same name for a different variable. And if you run it, it usually won't work correctly anyway ("dynamic" detection).
What is a readable ternary operator (conditional expression)?
I have discussed what makes a ternary operator readable or unreadable, and here I will summarize it:
- Do not use symbols or abbreviations
- Place the condition afterwards (on the right)
- Write it in the same order as a conditional block
Using the method developed this time, it looks like this:
const value =_( "A" ).elseIf( x == 0 ).then( "B" );
You might feel that the increased number of parentheses makes it look cluttered or hard to read, but that is not the essence. Structurally, it is simple, as shown below:
const value = "A" // However, when ... is ...
Ternary expressions are often used when there are exceptional conditions. For example, it looks like this:
const message = Boolean(name) ? `Name: ${name}` : "";
// ↓ Rewritten
const message =_( `Name: ${name}` ).elseIf(!name).then("");
// ↓ Meaning
const message = `Name: ${name}` // However, when ... is ...
By rewriting it this way, you can understand that the message variable is something that contains Name: ${name}. The fact that it might contain an empty string "" is exceptional, so it is unnecessary information at the initial stage of reading and understanding the code. Once I recognize that the code I am reading is an exceptional case, I discard that information from my head and proceed to find the expected information. You cannot move forward without discarding unnecessary data.
In sample code often introduced for learning purposes, exceptional processing is frequently omitted to make it easier to understand. This is equivalent to not writing the part after elseIf in the code above. Reading up to just before elseIf is sufficient for understanding.
const message = `Name: ${name}`;
Please try reading the code below, where the ternary operator is prohibited, from the beginning. Then, compare them. The code below tends to make you recognize the exceptional content in the first half as the main subject, and I don't think you can fully understand the content unless you "read and discard" that first half. Also, var can be replaced with const or let to write it appropriately.
if (!name) {
var message = "";
} else {
var message = `Name: ${name}`;
}
However, that alone is a feature also found in Python's conditional expressions (ternary operator). In this TypeScript library-like method, I developed it so that the order is the same as a conditional block: (Condition A is omitted), Value A, Condition B, Value B. In other words, the difference from Python is that if else has become elseIf then. This ensures that the written condition refers to Value B rather than Value A, making it readable in the same way as a conditional block and very easy to understand.
On the other hand, in Python's style, it becomes Value A, Condition A, Value B, so you would likely judge that you must also read the if belonging to the same A.
message = f"Name: {name}" if name // Otherwise...
Since I implemented the readable ternary expression as a method, parentheses and such become mandatory. However, even when using standard ternary operators, explicitly writing parentheses often makes the structure clearer and easier to read, so those who are concerned about parentheses will likely get used to it quickly. Without parentheses, aren't you momentarily confused by the === following x? You probably only managed to read it after finding the ? further ahead. Having parentheses around the values before and after the : makes for a more unified three-term style, and makes ? and : more prominent, helping the understanding that it is a ternary operator.
const value = x === 1 ? "A" : "B";
// ↓
const value = (x === 1) ? ("A") : ("B");
However, if there is no primary-secondary relationship between Value A and Value B such that one is an exception, you should write a general branch instead of using the postfix else if.
if (isNumber(nameOrID)) {
var message = `ID: ${nameOrID}`;
} else {
var message = `Name: ${nameOrID}`;
}
By the way, even with a ternary operator, using indentation might make it somewhat less difficult to read. With a ternary operator, you can also use const. However, you will always end up reading the conditional expression first, and since the variable name is written only once, it becomes visually harder to see that there are two types of values, which reduces reading efficiency.
const message = (isNumber(nameOrID)) ? (
`ID: ${nameOrID}`
) : (
`Name: ${nameOrID}`
);
Postfix else if is a way of writing that avoids making the reader process unimportant, exceptional information first, so it should only be used when there is a primary-secondary relationship. For example, suppose there is a specification to display an approximate hourly wage on a pay slip. The hourly wage is:
Payment / Working Hours
Right? This is all the information needed to understand the gist. "However," suppose there is also a specification to display the hourly wage as 0 if the working hours are 0 to avoid a division-by-zero error. In this case, 0 is a value determined only because the definition of payment/working hours exists, so there is a primary-secondary relationship. If there is a "however" in the specification text, you can consider it a primary-secondary relationship.
const hourlyWage = _(()=>( payment / workingHours )).elseIf(workingHours===0).then(0)
Note: Regarding ()=>( ___ ), please refer to the section on Lazy Evaluation.
If the code for the "however" specification spans many lines, it should be written as a general branch, just like with a ternary operator. There are many cases that do not have a primary-secondary relationship and are not targets for postfix else if. When there is no primary-secondary relationship, it is better not to use the ternary operator as it makes it clearer that there are two types of values. This kind of difference is not something to be improved by coding rules based on grammar, but rather something where readability is improved by writing skills.
Tentative Implementation in TypeScript
The introduction has been long, but I would like to introduce the implementation of the method for writing readable ternary expressions in TypeScript.
Observant readers might have already noticed that .elseIf and .then are method chains.
And as those even more observant might have wondered, there is an _ to the right of the equals sign. This is actually a call to a function named _. By writing _ where there would normally be whitespace, I am making it less conspicuous. Strict Lint settings might warn about the lack of space to the right of the equals sign, but please allow it to be ignored. Making every bit of syntax explicit is not necessarily the goal. Making the main subject clear is the priority. If clarity is needed, jumping to the definition of elseIf or then will reveal the logic. That is sufficient.
Here is the definition of the ElseIfMethodChain class, which includes the _ function and the .elseIf and .then method chains. (Note: This is not yet the final version.)
// Readable postfix else-if ternary operator method (tutorial version)
// Example:
// const value = _( data ).elseIf(data === "").then("empty");
function _(value: any): ElseIfMethodChain { // The function name is '_'
return new ElseIfMethodChain(value);
}
class ElseIfMethodChain {
constructor(
public value: any
) {}
elseIfCondition = true;
elseIf(condition: boolean): ElseIfMethodChain {
this.elseIfCondition = condition;
return this;
}
then(elseValue: any): any {
if ( ! this.elseIfCondition) {
return this.value;
} else {
return elseValue;
}
}
}
I will explain the execution sequence.
- Calls the
_function and returns an object of theElseIfMethodChainclass containing the argument's value. - Calls the
elseIfmethod of that object, stores the result of the condition in a property of the object, and returns the object again. - Calls the
thenmethod of that object and returns the appropriate value based on the result of the condition.
This tentative implementation works as expected in simple cases, but it has the following issues for practical use:
- The return value of
thenis of typeany, so Visual Studio Code's IntelliSense stops working. - Since values are evaluated before the method call, access exceptions for
undefinedornullobjects occur.
The Necessity of Lazy Evaluation
When using the methods developed this time to write readable ternary operators, it is sometimes necessary to pass an arrow function ()=>( ___ ) as an argument to perform lazy evaluation. (This wouldn't be necessary if it were part of the language specification.)
const value =_( object!.name ).elseIf(!object).then("");
// ↓
const value =_(()=>( object!.name )).elseIf(!object).then("");
As for the writing style, since the arrow function is secondary, it is written closely to the left, while the main content object!.name is emphasized with spaces and parentheses. Also, the ! in object!.name is necessary to avoid static analysis errors for the object, which is a null/undefined-nullable type. If you don't write it, the error will inform you. In other words:
- If you receive a warning regarding a null/undefined-nullable type, write
!to the right of the object and wrap it in an arrow function.
When you pass an arrow function (or an anonymous function), the content of that arrow function is executed at the timing it is called. In the code above, the execution of object!.name occurs after it is determined which value should be returned within then. This is what we call lazy evaluation here. This mechanism is used in almost every widely used testing framework, such as Jest, so you don't need to hesitate much about using it. Also, using an arrow function results in a slight slowdown due to function call overhead.
If you do not pass an arrow function, the execution of object!.name would occur before the conditional branch of !object (eager evaluation), resulting in an access exception for undefined or null objects. To avoid this, you pass an arrow function for lazy evaluation. In rare cases, you might need to pass an arrow function as an argument to then. Also, if no null/undefined-nullable objects are included in the expression to begin with, an arrow function is not necessary.
The problem where IntelliSense in Visual Studio Code stops working because the return value of then is of type any is avoided by excluding the any type using Generics and Conditional Types in the implementation of the ElseIfMethodChain class.
Incidentally, even if the return value is of type any, IntelliSense will function if you specify the type in the variable declaration to which the return value is assigned. However, this is not ideal because you won't be warned if you write the wrong type. You should use type inference actively. If you want to know the type, just move your mouse cursor over it.
const value: string =_( "A" ).elseIf( x !== 1 ).then( "B" );
// ↓
const value =_( "A" ).elseIf( x !== 1 ).then( "B" );
💡 The fact that eager evaluation occurs is self-evident since the postfix else if is a method, but since ternary operators are always lazily evaluated, some people might expect the postfix else if method to also evaluate lazily. However, this problem does not occur if you follow generally accepted good coding practices, which are:
- Do not write code with side effects in arguments.
- Do not call functions that perform recursive calls from arguments.
Those who leave confusing code with side effects in arguments, or those who write programs in fields where recursive calls are commonly used, will have to be careful themselves about whether to evaluate lazily. However, no cases have been found yet that fall under arguments with an exceptional relationship while also containing side effects or recursive code. If there is no exceptional relationship, writing a normal conditional branch is more suitable. Furthermore, one might think that if you have to be careful about whether to evaluate lazily (because side effects or recursion are needed), the implementation should just restrict it to always writing
()=>( ___ ). However, this would be counterproductive as it goes against the very effort of making the code readable. It is like Yoda notation, where you say "this way mistakes never happen," but in the end, there are conditions where you must write it that way. Note that these issues arise because postfix else if is not part of the language specification, not because of the postfix else if style itself.
💡 The postfix else if method has the following limitations:
- It cannot return a function.
This is because if you specify a function, it is called internally to return its result. However, if you try to call it thinking the returned value is a function, you will be warned statically, so there is no need for special caution. If you foolishly keep calling it while ignoring warnings and denying the specification that states it returns the result of the function, it is natural to introduce bugs. There is nothing to worry about when using the postfix else if method. However, you might find yourself looking for a way to return a function. To prevent wasting time on that, I have noted the limitations in the comments of the postfix else if implementation. If you absolutely must branch functions, please use normal conditional branching.
Detailed Explanation of Implementation
The implementation code for the postfix else if method is provided at the end. Here, I explain why the implementation turned out this way. I recommend reading this only if you are interested in the implementation.
While the code could be written with fewer characters by actively using infer, I have intentionally kept it redundant. This is because I couldn't find any websites or AI responses that could adequately explain what infer is. Even the official documentation only says it infers things "nicely." Although infer remains, I have made it so that you can understand what WidenType is even if you cannot read that part.
💡 Many intermediate programmers (self-proclaimed senior programmers who cannot mentor their members) judge whether code is readable based on the information already in their own heads. As a result, they tend to write code that is hard to read for first-time readers who lack that information, or for their future selves. Code that uses abstract and difficult syntax, symbols, and abbreviations—which are defined on top of many specific concepts—may seem readable to those who already have the information, but for those without it, that mysterious syntax, symbols, and abbreviations provide zero information, making it impossible to read. Such code becomes untouchable technical debt. It's not that the level of those around them is low, but that the level of the person who wrote it is low. It's not "I'm high-level because I can read this code"; it's "the person who wrote it is high-level." So, what should be done? One method is to write simple code even if it's redundant for cases that are easy to understand. If simple cases are understood and internalized, the difficult code becomes easier to grasp.
I have intentionally avoided supporting cases where the type for the true value (T) and the type for the false value (U) are different. Postfix else if is used to align the value in exceptional cases with the value in non-exceptional cases when there is an exceptional primary-secondary relationship. Therefore, returning a T|U type would mean not aligning the values, which is counterproductive. Even if it's not that case, it's just pushing the problem down the road, and some kind of branching will occur later anyway. In that case, it is more suitable to write a general branch consisting of a large block that includes that branching logic.
💡 Knowing what
T|Uitself is isn't difficult, but knowing what it means to returnT|Uis. With justT, you don't have to worry about what kind of type it is. WithT|U, you'll want to investigate the difference between T and U. Rather than making the interface definition difficult to understand for the sake of versatility, I believe it's better to be clear even if there are limitations. Accounting for cases that aren't necessary is excessive abstraction.
Implementation code for the postfix else if method in TypeScript
The code, including samples, can be found here.
// Readable postfix else-if ternary operator method
// Example:
// const value =_( data ).elseIf(data === "").then("empty"); // early evaluation
// const value =_(() => ( object!.name )).elseIf(!object).then(""); // lazy evaluation
// Limitation:
// "then" method cannot return function, because of lazy evaluation support.
// Declare any object type with "Any" type, because no warning is given that lazy evaluation is required.
function _<T>(value: T): PostfixElseIfMehodChain<WidenType<T>> { // The function name is '_'
return new PostfixElseIfMehodChain<WidenType<T>>(value as WidenType<T>);
}
class PostfixElseIfMehodChain<T> {
constructor(
public value: T
) {}
elseIfCondition = true;
elseIf(condition: boolean): PostfixElseIfMehodChain<T> {
this.elseIfCondition = condition;
return this;
}
then(elseValue: TOrFunctionType<T>): TOrReturnType<T> {
if ( ! this.elseIfCondition) {
if (typeof this.value === 'function') {
return this.value();
} else {
return this.value as TOrReturnType<T>;
}
} else {
if (typeof elseValue === 'function') {
const elseFunction = elseValue as ((...args: any[]) => any);
return elseFunction();
} else {
return elseValue as TOrReturnType<T>;
}
}
}
}
type TOrReturnType<T> =
T extends (...args: any[]) => any
? ReturnType<WidenType<T>>
: T;
type TOrFunctionType<T> =
((...args: any[]) => T)
| TOrReturnType<T>
| T;
type WidenType<T> = // e.g.) type 1 => type number, type "A" => type string
T extends number ? number :
T extends string ? string :
T extends boolean ? boolean :
T extends (...args: any[]) => infer R ? (
R extends number ? (...args: any[]) => number :
R extends string ? (...args: any[]) => string :
R extends boolean ? (...args: any[]) => boolean :
(...args: any[]) => any ) :
T;
type Any = Record<string, any> | null; // "any" type property object
type AnyOrPrimitive = Any | number | string | boolean;
Small but powerful effect
Since I started using ternary expressions to make exceptional cases less prominent, the amount of code I read only to discard from my mind has significantly decreased. This means development efficiency has more than doubled. Considering how much time is consumed when one gets confused, the development efficiency might even triple or quadruple. Please give it a try and experience it for yourself.
Discussion
主旨は理解できる。
しかしif無しにelseという言葉が出てくるのが変だし英文法的にも変だ。
elseIfではなくbutIfにするとよいのでは。
また、メソッドチェーンでelseIfを繋げられるのが変なので、まだ改良できそう。
という記述に新鮮な驚きを感じました。自分で今までそんなことを思ったことないし、周りでそう言ってる声も聞いた事がない。QAサイトにCを勉強中の人の「?って記号はどういう意味ですか?」的な質問を見て、「演算子くらい全部覚えろよ」と思ったことはありますが。つまり「どの言語でも、その言語に慣れてない人に読みにくい構文はいろいろあるよね」の中の1つで、その言語の初心者を脱した人であればそんなこと言わないだろうと。
Cの三項演算子の起源はおそらくAlgol60のif式
x = if 条件 then 値1 else 値2;で、AlgolからCまでの言語数代を経てどこかで記号にしたいが為にそうしたのでしょう。ifに該当する記号がないのは「無くても構文解析できるから」と言う理由で、読みやすさ観点ではなかっただろうと思います(特に調べておらず推測です)。Pythonの条件式は読みにくいというのには同意です。これはおそらくPerlの後置if(if修飾子)をヒントにしたのでしょうが、Perlの後置ifは式ではないので値を持たない。また式にする段階で else を追加したかったので変な形になってしまったのかと思います。Cのような記号の構文にしたくなかったのなら、最初に戻ってAlgolのif式と同じ形式にすれば大変分かり易いと思います。が、Pythonの設計思想はプログラムの書きやすさ・読みやすさより重視されているようなので、おそらく何らかの設計思想に従ってAlgolのif式の形式は採用できなかったのではないかと思っています(特に調べておらず推測です)。
つまり、後置ifもどきを、if式/3項演算子の代替に使ったPythonの方針は失敗だったのではないかと思います。まあ、Python設計者からすると成功なのでしょうが。繰り返しですが、普通にif/then/elseにしなかった理由が分からない。
Rubyだと、C風の
x = 条件 ? 値1 : 値2も使えるし、Algol風のx = if 条件 then 値1 else 値2 endも使えるし、またPerlと違って後置ifも値を持つのでx = (値 if 条件)も可能です(代入と後置ifの優先度の関係で括弧が必要)。後置ifは条件が偽の時の値はnil固定なので使える場面が限定的ですが。TypeScriptは深いところまで知らないので、その部分はほとんど読み飛ばしてますが、三項演算子のような制御構造を持った構文を「引数を全部評価してからメソッドを呼び出す」ことで代替するのは、お書きのように使える場所が限定されるので、嬉しい人は少ないのではないかと思います。また、「主従関係」というのは三項演算子を使うケースのごく一部だと思うし。
三項演算子の一部だけ書き換えるなら、全部三項演算子のままの方がずっと良いと思います。