DEV Community

Cover image for Typescript can be confusing
Simon Pfeiffer for Codesphere Inc.

Posted on • Updated on

Typescript can be confusing

At Codesphere, we code mostly in Typescript, not necessarily because it is our favorite language, but because we found out that it made us the most productive.

To start off, here are some benefits of Typescript that make us more efficient:

  • ability to code in the same language in both frontend and backend
  • (mostly) great OO + types
  • asynchronous code

However, recently I came across two very strange behaviors, (I know, they are common in the JavaScript Bubble) and I felt the urge to share them!

1: ['1', '2', '10'].map(parseInt);

I came across this when I wanted to format some user input, convert it into numbers and put them in a chart.

Alt Text

Don't believe me? Open up a console in your browser, paste in the following, and press enter.

['1', '2', '10'].map(parseInt);

This does not work, because map passes three arguments into parseInt() on each iteration. The second argument index is passed into parseInt as a radix parameter. So each string in the array is parsed using a different radix. '2' is parsed as radix 1, which results in NaN, '10' is parsed as radix 2, which is 3, and '1' is parsed as the default radix 10 because its index 0 is falsy.

2: Inheritance of 'readonly' in Typescript

During a code review at Codesphere, my colleague Roman came across the idea to make methods readonly. What happened next left us a little confused.

It's actually not possible to make a method readonly, but it is possible to make a readonly property with a function type, which has the same effect.

Interestingly enough, it is not possible to assign the property again for instances of the same class, but it is possible to inherit a class and override the property, as well as, assign the property on an instance of a subclass.

class Roman {
    readonly jonas: () => void = () => console.log("huh?");
}

class Elias extends Roman {
    jonas: () => void = () => console.log("oh no, override works!");
}

const elias = new Elias();
elias.jonas(); // oh no, override works!
elias.jonas = () => console.log("oh no, assignment works too!");
elias.jonas(); // oh no, assignment works too!
Enter fullscreen mode Exit fullscreen mode

That's all for now, I hope that you enjoyed the read! The support for TypeScript is one of the core features of Codesphere IDE. By the way - my name is Saji and I joined the Codesphere team because I love coding and our vision to improve the developer experience.

What is your experience with TypeScript? Feel free to share your story about the things you find confusing in TypeScript!

Top comments (26)

Collapse
 
peerreynders profile image
peerreynders • Edited

because we found out that it made us the most productive.

Some teams and projects are finding that they need TypeScript to get out of the way:

Moved some of my smaller libs to JSDoc TS; thoroughly recommend it. Among other things, the resulting code is generally smaller than transpiled code. Building, testing etc all become much less finicky. And .d.ts files are still generated from source code.

TypeScript: JS Doc Reference.

because map passes three arguments into parseInt() on each iteration.

Which is documented on MDN: Array.prototype.map() and MDN: parseInt() (and in the ECMAScript spec Array.prototype.map(), parseInt()) - and I imagine the signatures are revealed by IDEs like Visual Studio Code (which I personally don't use).

Ultimately this isn't a TypeScript issue but a lack of familiarity with the ECMAScript runtime environment.

Which leads to another observation (Here is why you might NOT want to use TypeScript — Part 3: TypeScript is not JavaScript):

Ultimately this is a leaky abstraction and developers must understand the concepts of JavaScript to debug and truly understand their TypeScript code.

Granted Microsoft admits:

TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the programmers at Microsoft could bring traditional object-oriented programs to the web.

It's more pragmatic to not think of TypeScript as a programming language. I think Hongbo Zhang of ReScript said it best:

TypeScript is designed to be a JavaScript super linter, it uses type system to give your better editing experience, it has seamless interop with JavaScript, at the same time, it inherits all JavaScript gotchas.

And given - A Note on Soundness:

TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”.

i.e. many developers are deriving a false sense of security from TypeScript. So regardless of whether TypeScript is used or not - acquiring JavaScript/ECMAScript competence remains essential.

This is especially true for developers coming from a traditional OO background like Java and C# because going straight to TypeScript leads to a sense of familiarity that is simply not warranted.


It's actually not possible to make a method readonly, but it is possible to make a readonly property with a function type, which has the same effect.

... and this illustrates my previous point.

From my perspective I'm confused what you hope to accomplish by making a method readonly. From the context I'm guessing you're after Java's final or C# sealed.

That is not what readonly is for - Readonly:

Constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned.

Furthermore an object method and a function assigned to an object property are fundamentally different mechanisms which is why the latter can be declared readonly and the former cannot.

Object methods are typically functions that are assigned to properties on the prototype object that is potentially shared by many object instances that use it as their prototype. It's a reuse mechanism because all these objects share the same functions (i.e. methods) via the prototype. But given that these objects don't actually own the properties that the functions are assigned to there is no way to make them readonly.

However when a function is assigned to an object's field, it's simply a function value assigned to that particular object. And because it's on the field, it's possible to declare it readonly - which constrains it to be assigned in the constructor or when the object literal is initially created.

And the way the prototype chain works, an object's fields will always be inspected before the prototype object - so function values on objects will always "override" methods on the prototype object - so a readonly on the prototype object won't stop an object assigning its own function value.

Another thing to be aware of is that arrow functions assigned to public class fields are not accessible via super in derived classes (example - because arrow functions bind this to the creating scope - so they can't be used as methods).

It should be noted that ECMAScript is object-oriented in the sense that the programmer codes actual objects while in languages like Java and C# the programmer codes classes - i.e. practices class-based object-orientation instead.

ES2015 introduced class:

Classes are a template for creating objects.

In a class-based OO language class-membership is static and for the lifetime of the class instance. In ECMAScript a "class" is a creational convenience - once created it is possible to a alter and augment a particular object to fit the specification of another "class" while the identity of the object remains unaffected.

From that point of view I'm deeply concerned when someone is trying to emulate concepts like "final" and "sealed" in ECMAScript/TypeScript because that implies an attempt to practice traditional object-orientation which in my view doesn't align with the spirit of the language.

I see ECMAScript as Function-Oriented and that modules are a far more important mechanism for establishing boundaries than classes.

A lot of ECMAScript programming relies far more heavily on closures compared to traditional OO programming (OOP with Functions in JavaScript) but as they say:

Closures are a poor man's object..

That being said the root cause of the confusion in the article regarding TypeScript/ECMAScript is likely trying to make it into something which it is not.

Collapse
 
fkhadra profile image
Fadi Khadra

Regarding your first example here is a good article on this jakearchibald.com/2021/function-ca...

Collapse
 
romfrolov profile image
Roman Frolov

Good one! Thanks for sharing.

Collapse
 
simoncodephere profile image
Simon Pfeiffer

cool :) thanks for the tip!

Collapse
 
pclundaahl profile image
Patrick Charles-Lundaahl

You're trying to mimic something like final in, e.g., Java, right? I know I've definitely found myself wishing for that before!

I feel like you might be able achieve the runtime behavior you're after with a proxy. You wouldn't get the type safety aspect, though, and I reckon the implementation would be ugly enough that you'd never want to use it.

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Instead of parseInt

['1', '2', '10'].map(x=>~~+x)
Enter fullscreen mode Exit fullscreen mode

Works - and shorter

Collapse
 
peerreynders profile image
peerreynders
> ['1', '4294967295', '10'].map(x=>~~+x)
(3) [1, -1, 10]
Enter fullscreen mode Exit fullscreen mode

Bitwise NOT (~):

The operands are converted to 32-bit integers and expressed by a series of bits (zeroes and ones). Numbers with more than 32 bits get their most significant bits discarded.

Unsigned right shift (>>>):

Unlike the other bitwise operators, zero-fill right shift returns an unsigned 32-bit integer.

Know the limitations of your tools.

Integers and shift operators in JavaScript

> ['1.1', '9007199254740991.6', '10.9'].map(n => parseInt(n))
(3) [1, 9007199254740991, 10]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jonrandy profile image
Jon Randy 🎖️

I know all that, but for the purposes of a simple string -> integer parser, this works just fine

Collapse
 
baenencalin profile image
Calin Baenen

The hell did I just read?

Collapse
 
natelindev profile image
Nathaniel

that works, but unreadable, therefore it's a bad practice, should be ['1', '2', '10'].map(n => Number.parseInt(n));

Collapse
 
tomaszs2 profile image
Tom Smykowski • Edited
Collapse
 
sightlessdog profile image
Elyess Eleuch

Thanks for the article, I started learning typeScript a week ago. And I guess that I am gonna need this article in the next few days :p

Collapse
 
romfrolov profile image
Roman Frolov

Awesome! Don't forget to share your experience. :)

Collapse
 
simoncodephere profile image
Simon Pfeiffer

Awesome! Happy to be of help :)

Collapse
 
simonholdorf profile image
Simon Holdorf

Thanks for the post :)

Collapse
 
simoncodephere profile image
Simon Pfeiffer

glad you enjoyed it!

Collapse
 
lico profile image
SeongKuk Han

Thanks for your tips.
What a beautiful way to convert string to number..

['1', '2', '10'].map(Number);

It works. If I didn't see your post, I wouldn't know it.

Collapse
 
peerreynders profile image
peerreynders • Edited

Please observe:

> ['1.2', '2.3', '10.11'].map(Number)
(3) [1.2, 2.3, 10.11]
Enter fullscreen mode Exit fullscreen mode

versus

> ['1.2', '2.3', '10.11'].map(n => Number.parseInt(n))
(3) [1, 2, 10]
Enter fullscreen mode Exit fullscreen mode

With software the "devil is always in the details".

Martin Fowler

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Refactoring: Improving the Design of Existing Code, 2nd Edition p. 10 (2018)

With that in mind when I see parseInt in code that I review, I have to conclude that floating point values are not welcome and that the range of values from Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER is considered sufficient for the purpose of the functionality.

Ultimately I prefer:

const toInteger: (v: string) => number = (value) => Number.parseInt(value);

function doSomething(...values: string[]): number[] {
  return values.map(toInteger);
}

console.log(doSomething('1.2', '2.3', '10.11'));
Enter fullscreen mode Exit fullscreen mode

Clearly it's more verbose but the added side benefit is that you aren't at the mercy of the JavaScript engine realizing that it can just reuse the same function - rather than creating a new anonymous function every time doSomething is invoked. (JavaScript performance is notoriously unpredictable and inconsistent).

In this way I actually find that TypeScript is an impediment to refactoring.

While it seems perfectly capable of inferring a function type when the function is inlined, it needs explicit type declarations once you factor it out (something ReasonML/ReScript doesn't tend to have trouble with) which also means that the source becomes more "noisy".

In my personal opinion inline functions are more imperative because they focus on the "how" of the functionality. With named functions you have an opportunity to be more declarative because the name can relate to the "why".

In pure ECMAScript I prefer:

function toInteger(value) {
  return Number.parseInt(value);
}
Enter fullscreen mode Exit fullscreen mode

because I find function declaration hoisting useful - it gives you more freedom of how to order things within a module.

But in TypeScript:

function toInteger(value: string): number {
  return Number.parseInt(value);
}
Enter fullscreen mode Exit fullscreen mode

The equivalent to ECMAScript code exists in the "value space" - TypeScript types exist in "type space". With function toInteger(value: string): number both are conflated (in the tradition of languages with a C-style syntax).

So in TypeScript I often find myself trading-off hoisting:

const toInteger: (v: string) => number = (value) => Number.parseInt(value);
Enter fullscreen mode Exit fullscreen mode

to separate the "type space" function type (v: string) => number from the "value space" definition const toInteger = (value) => Number.parseInt(value);.

Collapse
 
lico profile image
SeongKuk Han

Thank you! OH Yep,,, I didn't think about floating number.
Anyway, I agree with your opinion.
Thanks again, I learned a lot from your comment.

Collapse
 
simoncodephere profile image
Simon Pfeiffer

thanks for the insight! will keep that in mind for floating-point numbers :)

Collapse
 
eliasgroll profile image
Elias Groll • Edited

Nice post!

JS/TS are great but they have their legacy..

Looking forward to new TS versions as well as Deno, if it ever gets adopted :)

Collapse
 
timbotimber profile image
Tim Stephens

The way Typescript works with classes was one of the things I found the most interesting about the language when I started learning it. Very cool + interesting article!

Collapse
 
simoncodephere profile image
Simon Pfeiffer

Thanks, Tim :) Please do share if you discover anything else cool!

Collapse
 
andrewbaisden profile image
Andrew Baisden

TypeScript takes some getting used to but its quite good once you are used to it :)

Collapse
 
amn3s1a2018 profile image
Amn3s1a2018

Actually it's TypeScript with a capital S

Collapse
 
changerhe profile image
Changer He

['1', '2', '10'].map(parseInt);
equalTo
['1', '2', '10'].map((value, index) => parseInt(value, index));
so, when value is 2, index is 1, that means:
parseInt('2', 1) // NaN