DEV Community

Cover image for Key Headaches in TypeScript
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Updated on

Key Headaches in TypeScript

After many years of doing "regular" JavaScript, I've recently (finally) had the chance to get my feet wet in TypeScript. Despite some people boldly telling me that "I'd pick it up in 5 minutes"... I knew better.

For the most part it is fast-and-easy to pick up. But switching to a new paradigm always gets hung up around the edge cases. TypeScript has been no exception to this.

I already wrote two long posts about the hurdles I had to jump through just to get React/TS to define default prop values under the same conventions that are common (and easy) with React/JS. My latest conundrum has to do with the handling of object keys.


The Problem

When I'm using JavaScript, I frequently have to deal with various objects. If you've done any JS development, you know I'm not talking about "objects" in the same way that, say, a Java developer talks about "objects". The majority of JS objects that I seem to encounter are more equivalent to hashmaps - or, on a more theoretical level, tuples.

For example, it's quite common for me to have two objects that might look like this:

const user1 = {
  name: 'Joe',
  city: 'New York',
  age: 40,
  isManagement: false,
};

const user2 = {
  name: 'Mary',
  city: 'New York',
  age: 35,
  isManagement: true,
};
Enter fullscreen mode Exit fullscreen mode

Nothing too complex there, right? Those "objects" are just... data structures.

So let's now imagine that I often need to find what any two users have in common (if anything). Because my app requires this assessment frequently, I want to create a universal function that will accept any two objects and tell me which key values those objects have in common.

In JavaScript, I could quickly crank out a little utilitarian function like this:

const getEquivalentKeys = (object1: {}, object2 = {}) => {
   let equivalentKeys = [];
   Object.keys(object1).forEach(key => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

[NOTE: I realize that this could be done even more efficiently with, say, a good .map() function. But I think this is a bit clearer (meaning: more verbose) for the purposes of this illustration.]

With the function above, I can now do this:

console.log(getEquivalentKeys(user1, user2));
// logs: ['city']
Enter fullscreen mode Exit fullscreen mode

And the function result tells me that user1 and user2 share a common city. Pretty dang simple, right??

So let's convert this to TypeScript:

const getEquivalentKeys = (object1: object, object2: object): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

This "looks" right to me, except... TS doesn't like it. Specifically, TS doesn't like this line:

if (object1[key] === object2[key]) {
Enter fullscreen mode Exit fullscreen mode

TS says:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.

Hmm...

To be clear, I know that I could easily use an interface to define the user type and then declare it in the function signature. But I want this function to work on any objects. And I understand why TS is complaining about it - but I definitely don't like it. TS complains because it doesn't know what type is supposed to index a generic object.


Alt Text

Wrestling With Generics

Having already done Java & C# development, it immediately struck me that this is a use-case for generics. So I tried this:

const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

But this leads to the same problem as the previous example. TS still doesn't know that type string can be an index for {}. And I understand why it complains - because this:

const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
Enter fullscreen mode Exit fullscreen mode

Is functionally equivalent to this:

const getEquivalentKeys = (object1: object, object2: object): Array<string> => {
Enter fullscreen mode Exit fullscreen mode

So I tried some more explicit casting, like so:

const getEquivalentKeys = <T1 extends object, T2 extends object>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      const key1 = key as keyof T1;
      const key2 = key as keyof T2;
      if (object1[key1] === object2[key2]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

Now TS complains about this line again:

if (object1[key1] === object2[key2]) {
Enter fullscreen mode Exit fullscreen mode

This time, it says that:

This condition will always return 'false' since the types 'T1[keyof T1]' and 'T2[keyof T2]' have no overlap.

This is where I find myself screaming at my monitor:

Yes, they do have an overlap!!!


Sadly, my monitor just stares back at me in silence...

That being said, there is one quick-and-dirty way to make this work:

const getEquivalentKeys = <T1 extends any, T2 extends any>(object1: T1, object2: T2): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

Voila! TS has no more complaints. But even though TypeScript may not be complaining, I'm complaining - a lot. Because, by casting T1 and T2 as any, it basically destroys any of the wonderful magic that we're supposed to get with TS. There's really no sense in using TS if I'm gonna start crafting functions like this, because anything could be passed into getEquivalentKeys() and TS would be none the wiser.

Back to the drawing board...


Alt Text

Wrestling With Interfaces

Generally speaking, when you want to explicitly tell TS about the type of an object, you use interfaces. So that leads to this:

interface GenericObject {
   [key: string]: any,
}

const getEquivalentKeys = (object1: GenericObject, object2: GenericObject): Array<string> => {
   let equivalentKeys = [] as Array<string>;
   Object.keys(object1).forEach((key: string) => {
      if (object1[key] === object2[key]) {
         equivalentKeys.push(key);
      }
   });
   return equivalentKeys;
}
Enter fullscreen mode Exit fullscreen mode

And... this works. As in, it does exactly what we'd expect it to do. It ensures that only objects will be passed into the function.

But I gotta be honest here - it really annoys the crap outta me. Maybe, in a few months, I won't care too much about this anymore. But right now, for some reason, it truly irks me to think that I have to tell TS that an object can be indexed with a string.


Alt Text

Explaining To The Compiler

In my first article in this series, the user @miketalbot had a wonderful comment (emphasis: mine):

I'm a dyed in the wool C# programmer and would love to be pulling across the great parts of that to the JS world with TypeScript. But yeah, not if I'm going to spend hours of my life trying to explain to a compiler my perfectly logical structure.


Well said, Mike. Well said.


Alt Text

Why Does This Bother Me??

One of the first things you learn about TS is that it's supposedly a superset of JavaScript. Now, I fully understand that, if you desire to truly leverage TS's strengths, there will be a lotta "base" JS code that the TS compiler won't like.

But referencing an object's value by key (a type:string key), is such a simple, basic, core part of JS that I'm baffled to think that I must create a special GenericObject interface just to explain to the compiler that:

Yeah... this object can be indexed by a string.


I mean, that works. But if that's the way I'm supposed to do this it just makes me think:

Wait... what???


It's the same kinda annoyance I'd have if you told me that I have to explain to TS that a string can contain letters and numbers and special characters.

Now that I've figured out how to get around it, I suppose it's just one of those things that you "get used to". Or... maybe there's some simple technique in TS that would allow me to get around this (without disabling TS's core strengths). But if that magical solution exists, my paltry googling skills have yet to uncover it.

Top comments (20)

Collapse
 
pallymore profile image
Yurui Zhang • Edited

I think the Record utility type could help you here:

const getEquivalentKeys = <
    T extends Record<string, any>,
    P extends Record<string, any>
>(
  object1: T,
  object2: P,
): string[] => {

Now you can get even stronger static type checking with the help of another utility type Partial :

const getEquivalentKeys = <
    T extends Record<string, any>,
    P extends Partial<T>
>(object1: T, object2: P): string[] => {

since this function aims to get shared keys with the same values, given a type T - we require that P must contain partially the same keys from T - now TS is able to give you errors before you try to compare two totally different objects:

const o1 = {
    foo: 'bar',
    hi: 'there'
};

const o2 = {
    bar: 'baz',
}

const o3 = {
    bar: 'baz',
    foo: 'foo',
}

getEquivalentKeys(o1, o2); // Error! o2 does not contain any of o1's keys
getEquivalentKeys(o2, o3); // Good
getEquivalentKeys(o1, o3);  // Good

We can do even better here with a more specific return type. Instead of saying it can be an array of any strings, we can assure TypeScript that the result only include shared keys betweenT and P => (keyof T & keyof P)[]

The final function:

const getEquivalentKeys = <T extends Record<string, any>, P extends Partial<T>>(
  object1: T,
  object2: P,
): (keyof T & keyof P)[] => {
  const equivalentKeys: string[] = [];
  Object.keys(object1).forEach(key => {
    if (object1[key] === object2[key]) {
      equivalentKeys.push(key);
    }
  });
  return equivalentKeys;
};

Given your example:

const result = getEquivalentKeys(user1, user2);

without running the code, your editor already knows what are the possible values in the results. (hover your cursor over it to see!)

typescriptlang.org/docs/handbook/u...

Collapse
 
pallymore profile image
Yurui Zhang

I think what might have caused the confusion here is the type object - in TS it's not what you think it is.

The object type refers to anything that is not a number, string, boolean, symbol, null, or undefined (the primitive types). It's actually very similar to any - just a tiny little bit narrower. I rarely find it useful and basically treat it the same way as any (which means - avoid at all costs).

I guess it can be used if you just want an object and don't really care what fields it has (or does it have any fields at all - that's why there are no index signatures on it). In this case since we do care about the parameters having strings as keys, object is not a good fit here.

Collapse
 
leoat12 profile image
Leonardo Teteo • Edited

Thank you very much for this. I like TS a lot, but I'm still learning and there are some things that you only learn after looking up a lot or by reading the entire docs. I already visited TS docs many times, but never saw this Utility Types section. They should put it more up in the list, in bold red letters. hahaha

Collapse
 
gillchristian profile image
Christian Gill

Notice that the type object is not the same as the type {}

typescriptlang.org/docs/handbook/b...

The type { [key:string]: any } is indeed what you want when referring the "JS object" that is more of a hashmap or record.

without disabling TS's core strengths

any basically does that. So a better way would be to use { [key:string]: unknown }

Also, if defining it as an interface is something that bothers you (rightfully so) know that it needn't be defined as a separate type, it could be inline in the function declaration.

const getEquivalentKeys = (object1: { [key:string]: unknown }, object2: { [key:string]: unknown }): Array<string> => {
   // ...
}
Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Notice that the type object is not the same as the type {}

I'm not seeing any reference to this in the link that you provided. Also, when you define something as type object, TS's own error messages refer to it as type {}. So while I freely admit that maybe there's something that I'm just not "getting" here, it would seem that object is indeed identical to {}??

any basically does that. So a better way would be to use { [key:string]: unknown }

In this scenario it may be better, since we're just comparing the values. But as a general way to define objects, that wouldn't work, because unknown would keep us from setting any new value on the object.

Also, if defining it as an interface is something that bothers you (rightfully so) know that it needn't be defined as a separate type, it could be inline in the function declaration.

Good point. Thanks for demonstrating the shorthand.

Collapse
 
sirseanofloxley profile image
Sean Allin Newell

I feel this pain. I've been doing TS for a few years now, and the best strategy I've come up to tackle things like this is to write my function, write out a hindley minler type annotation, write a return TS type, and then see how the inputs and outputs relate.

Sometimes the TS Handbook's advanced types help, sometimes libraries like fp-ts help, and sometimes i just sprinkle some any until I come back tmrw or next week with more experience and find a better+simpler expression.

I think there's value in this struggle, keep on it! TS is very keen on helping us write higher quality code (ie type correctness) with confidence.

I don't know who told you you'd pick it up right away; but I helped lead a hardcore C# team to a new TS project recently and it was definitely not easy.

<3

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Excellent feedback! And FWIW, this "headache" doesn't inspire me to drop TS. But sometimes it just feels to me like I'm "defining" things that really shouldn't need to be defined (e.g., generic object keys).

I know there's value in the "come back tmrw or next week with more experience and find a better+simpler expression" advice. If I were on a project with a critical deadline, I'd probably follow it. The blessing right now (or maybe it's a curse) is that we have some "down time" while they're assembling the team. And since we don't have to deliver anything right now, it definitely stokes my "developer sense" whenever I'm tempted to slap an any on something and just move on. In the long run, that will be valuable. In the short term, it's... frustrating.

As for the C# experience, I certainly understand that as well. I wrote a lot of C# - but it's been several years. And, as you've alluded to, even if I were coming straight from C#, I'm not sure that all of the translations would be second-nature, even if the syntax of TS looks much more familiar to a C# dev.

Cheers!

Collapse
 
merri profile image
Vesa Piittinen • Edited

I'm becoming increasingly uneasy of TypeScript. In the other hand it has benefits: it brings out stuff to beginner programmers so that they can get a bit faster into how things work, you can get more aid when getting into a new codebase so you can in theory become productive faster, and in theory it is nice to be strict.

But then you have this other side into it. There is a major cost into getting to know everything about it so that you can use it effectively. This means once you've paid the price to learn it and become expect, you sure want to praise it and push everyone else to using it.

You can also argue that it makes new developers too reliant of their IDE and tooling. And worse yet, people start making technology and architecture choices based on the amount of convenience they get and all the other stuff has to wrap around that convenience.

All this together seems to result into people who have almost a cult following of TypeScript. They give it more credit than it deserves, and think it produces more value than it actually does.

On personal level I don't like TypeScript as one of the things that I've always loved about JavaScript is the loose typing and the cleanish syntax you get that way. Of course, I've paid the price of learning how to read types implied from the individual code lines, and to do that fast. Makes me want to stick to that.

React deprecating defaultProps in future sounds bad to me as well. I guess I'm officially becoming a dinosaur :) Heck, I use Sublime Text and find my way around using Search from files.

Collapse
 
jwp profile image
John Peters

I grew up as a strong typer starting with Pascal in 1985, then Java, then C# and moved to JavaScript about 10 years ago before landing on Typescript.

Those first years in JavaScript killed me. Why? because the tooling for JavaScript, in particular intellisense was absent. The named object was the closet to a class but it could have any key and any value. I now understand and appreciate this looseness but it's this trait that lead to object cloning and copying just to get the shape and intellisense.
Talking about adopting style to the tooling.

The hardest thing for me to grasp in JavaScript was the object. It had no constructor and nobody used Object.Create. It took me months to get the key value concept. An array of key value pairs. Not slotted property names with values. And no value types. It took me a while to get the super easy JavaScript. I could see objects everywhere but didn't know how to discover the key names. This made debuging object values impossible at first. Totally a turn off for first impression.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

Oh, man... Many good points here.

First, I freely admit that TS-vs-JS is largely a question of paradigm. Or, if you prefer, of mindset. And moving from C# to JS is definitely a shift in paradigm. I fully believe that many people love TS because they're already in that mindset. It took me quite awhile to come around to jQuery because something about that pattern just didn't grok with my mindset at first (although I eventually grew to love it). I jumped onto React fairly early because, for many reasons that would take too long to spell out here, it just kinda "fit" with my mindset.

Although "mindset" is an obviously-subjective idea, it's still a key factor in why some devs love Technology X, but chafe at Technology Y. This is why I try to be very careful to say that TS isn't "bad" and JS isn't "good". It's largely a product of each individual dev's mindset.

My mindset is probably a bit different from yours because, although I've done years of Java & C# (and genuinely enjoy many aspects of C# - Java... not so much), I started programming with server-side scripting languages. First PHP (which I still enjoy, even if it's acquired a bad rep), followed by a sizable chunk of ColdFusion (yuck).

Second, I love your description of the "disconnect" when, say, a Java developer talks about an "object" versus when a JavaScript dev uses the same terminology. For example, PHP has always had very powerful support for associative arrays (hashmaps, if you will). So when I finally started doing heavy JS dev, it even took me quite a while to grasp what JS means when it refers to an "object". Once I got that concept down, JS objects were incredibly easy and intuitive - but it absolutely took me a little while to "get there".

Finally, you hit upon a key subject when you talked about "tooling". Tooling isn't a "nice to have" or an "afterthought". A great language - with crappy tools - can seem like torture. Middling languages can feel much more intuitive with the proper tools.

I genuinely like C#, but I've long contended that the best thing about C# isn't anything specific about the language, per se. IMHO, what converts C# from a "passable" language (that will basically do what you'd expect from almost any OOP language) into a powerhouse is... Visual Studio. Say what you want about Microsoft tools in general, but I honestly believe that VS is something that they genuinely got "right" - from the very beginning.

Even amongst some of my hardcore C#-dev friends, I've asked them:

Is there something (any thing) about C# that somehow makes you genuinely enjoy typing it out? Or, to put it another way, would you be just as happy to write C# code in Notepad++? Or do you enjoy C# because you're writing it in Visual Studio??

Of those to whom I've posed this question, every one of them has admitted that their enjoyment of C# is largely tied to the default IDE (Visual Studio) that nearly every C# dev uses.

What does this have to do with this particular discussion?

Well, when you first started doing JS, the tooling for it was weak/nonexistent. And when you combine dynamic/weak/nonexistent typing with a glorified text editor, that has no ability to tie them together for you with something like Intellisense, the experience can be downright miserable.

It was about 6 years ago that I first discovered the JetBrains suite of products. I'm not exaggerating when I say that it literally transformed the way that I coded, because it was so damn intuitive at hooking me up to all of the existing classes/objects/functions/etc that I'd already written.

Without a tool like WebStorm (or VS Code), writing in any dynamic language is, IMHO, a royal PITA. But with a tool like that, I've honestly come to feel that dynamically-typed languages can be downright enjoyable.

(FWIW, my assessment on this isn't limited to dynamically-typed languages. I won't even consider taking a Java gig if it's one of those kinda dev shops that wants to dictate to me that I'll use Eclipse. I can't possibly explain to you just how much I loathe Eclipse. IMHO, IntelliJ isn't just "better". It's a quantum leap forward.)

I bring all this up because, to me, when someone complains about not knowing what to pass into a constructor/method/function, or not knowing what's being passed into that constructor/method/function, all I can think is, "Umm... Why don't you just use WebStorm???" One of the other commenters on this thread (@marktalbot) also alluded to this, with no prompting from me. A lot of what people claim to be "getting" with TS seems like a non-starter to me, because I know that those helpful IDE squigglies are available to me, in WebStorm, even when I'm "just" writing in JavaScript.

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

As your journey continues, I'm beginning to wonder why again... What is nirvana here? Somewhere down the road, a developer won't accidentally call something with an incorrect parameter? Then they don't notice, and something gets released with a bug. Is that it? Do we have examples of where this happened a lot, and people had disasters that have gone away and that productivity has increased? I'm sure we have anecdotes, but I'm wondering if since TS the web is suddenly a more productive place in terms of output - for sure everyone writing TS is writing a lot more words into their script.

For a while, I've been saying, oh, we can use some TS soon. Write some modules in it; we can use the typings from the JSDoc, etc. Now it's starting to feel like a way of getting people from other languages to feel "comfortable" rather than benefitting from the speed of authoring and terseness of pure JS. But maybe I'm just grumpy today. I should point out I don't type ";" because they slow me down, so I'm probably a basket case already.

Or do we think a set of documentation that tells you "what not why" is going to be helpful? You know, JSDoc has all of the types, but it also has examples, descriptions of the purpose, etc. I'm beginning to think of TypeScript as the latest version of JScript - one of Microsoft's disastrous attempts to own the language, causing years of pain for developers writing five versions of everything.

I guess if I had 1000s of developers and some of them were slapdash and junior, then it might help. However, I architect all of my systems to use loose coupling and minimal imperatives between modules. It seems to solve a lot more problems.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

I'm with you. Speaking for myself, I'm doing this because the current project will be in TS and the lead on the project loves TS and the other devs on the project love TS. So... I'm using TS. And maybe my attitude will change on it in a few months, but right now I'm having a really hard time seeing the benefit.

I've found it striking how much some people swear by it though. We had a little group discussion about it on our team because one of the other senior guys questioned why we're using TS. It turned out that, basically, all of the "younger" guys were strongly in favor of TS. And to be honest, it felt a little like hearing people who are brainwashed, cuz they all spoke with general statements about how things were "easier" or "cleaner" or had fewer bugs in TS - but these were all general platitudes with little in the way of empirical evidence. So we're using TS...

Here's the ironic thing. Several of the guys are new to React. (Whereas I'm basically the opposite - very senior in React, but new to TS.) But they're running into some of the same head-scratchers. I was hoping that, as guys with much more TS experience, they'd look at what's driving me nuts and say, "Oh, yeah - you just need to do it this way." But they're not doing that. They're stumbling over some of the same things.

Your point about JSDoc is very interesting. When you wrote that in a previous blog comment, it kinda caught me off guard. I was thinking, "Well, regardless of what you think of TS, you can't really compare it to JSDoc cuz JSDoc is really just like... linting. I mean, it doesn't actually do anything in your code. It basically just 'speaks' to your IDE to help with your coding." But then I realized that, in many respects, that's what TS is doing, right?? I mean, TS just compiles down to plain ol' JS. All that TS "magic" is really just there to help you hook things up in your IDE.

Your last paragraph is perhaps most telling. I think that, perhaps, some of my resistance to TS is that I'm (too) senior, and a lot of what TS is trying to "fix" is stuff that I've already figured out how to fix just by writing better code. I'm still open to the idea that maybe TS is better than I think it is once we account for the fact that most devs on a team will not have 20+ years of experience.

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

I think you put your finger on my problem here. I understand why I need strong types in C++, it's because I am in control of the memory allocated for every single variable and what it's doing and exactly how it will be interpreted by the function which receives it. That's all just a sham in TS, it is only there for linting. IntelliJ does interpret JSDoc and dynamically compiles my code and warns me if I'm using the wrong variable. But I only have to do it when I want to. Maybe that's the problem... you have no choice in TS so you can't short cut it.

Collapse
 
dinsmoredesign profile image
Derek D

Similar experience here. We decided to adopt TS on a library rewrite after looking at the code beforehand and realizing there was a lot of weird things happening and it wasn't really clear HOW things were still working 🤣

I think TS definitely helped me understand the code as I was rewriting it, but I'm honestly not sure that it really made the code less error prone. I've been writing JS for a while now and type issues aren't generally something I run into. I understand and appreciate the flexibility of JS and its dynamic typing. A lot of developers I know see it as a hindrance to better code, but after rewriting this library in TS, I'm still not really convinced.

Don't get me wrong, the extra documentation we gain from having TS in the IDE is pretty awesome, but I don't know if I'd want to use TS on everything. On this library, it worked well and I think the annoyances were worth the effort, but if I were using it inside our UIs, I feel like it'd add more overhead than it's worth.

The real benefit of TS would be if it did type checking at RUNTIME, but since it doesn't, it kinda seems pointless in many cases. If you're able to have control over all your own data, it's one thing, but if you're using an external API and they changed something you didn't anticipate, your code would still have the same issues as it would've without TS.

Moreover, I think the pitfall many developers fall into with TS is over-typing everything. One of our junior devs worked on a specific class and functionality in our rewrite and when I went to code review it, TS definitely didn't make his code easier to understand - quite the opposite. I think TS has a place in some applications but typing things just because is a little silly.

FWIW, our library also went from ~6 files to over 80 with TS. It's definitely more organized than before (it would've been more files, regardless), but I'd say about 60% of the time was spent writing Interfaces to correctly pass the right types so TS wouldn't complain, not actually writing logic 😋

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis • Edited

Yeah... this. Sooooo much this.

I've been writing JS for a while now and type issues aren't generally something I run into. I understand and appreciate the flexibility of JS and its dynamic typing. A lot of developers I know see it as a hindrance to better code, but after rewriting this library in TS, I'm still not really convinced.

Exactly. For years, before I was writing any TS code, one of my common retorts to the TS crowd was that dynamic typing isn't a bug. It's a feature. Can it cause problems sometimes? Sure. But these aren't problems that I typically run into. And now that I'm diving into TS, it seems to be causing its own "class" of problems...

I'm not hating on TS. I'm definitely not saying it's "bad". But neither do I believe it's the kinda panacea that some devs seem to characterize it as.

The real benefit of TS would be if it did type checking at RUNTIME, but since it doesn't, it kinda seems pointless in many cases.

BINGO! I've even got a few articles queued up in my head around this exact topic. Even though I knew, on some level, that TS was purely a compile-time tool, the limitations of that didn't really slap me upside the head until I started writing TS. You see, in my code, I'm accustomed to adding many runtime checks. In my functions, there are many cases where I actually want the app to throw an exception or, at a minimum, to return out of the logic if the proper value types have not been provided.

But all of my runtime checks are still required in TS - cuz ultimately, TS doesn't even really "exist" at runtime.

but if you're using an external API and they changed something you didn't anticipate

Double BINGO! Basically, if you're dependent upon any kinda side-effects - API data, database data, state data, etc. - none of those lovely TS type declarations are gonna do you much good.

TS definitely didn't make his code easier to understand - quite the opposite.

<NoddingHead/> Even in some of my early TS articles, there have been some (awesome) people who've helpfully been chiming in with their code examples. And of course, I totally appreciate that! But sometimes you look at their solutions and they look a lot like complex regular expressions.

In fact, the more TS I do, the more I feel like RegEx is a useful analogy. RegEx is powerful. At the right time, RegEx can absolutely be the "right tool for the job". And of course, there are some things that you simply can't do without RegEx. But anyone who tells me that RegEx is easy to read is just a hardcore RegEx jockey. Even after 20+ years in this career field, there are still some times when I have to do a "deep think" whenever I'm trying to parse someone else's regular expression. TS is often the same.

I think TS has a place in some applications but typing things just because is a little silly.

I agree. And I think that I'm already having a bit of a problem with this. You see, a big part of my dev brain says, "Well, if you're gonna use TS, and TS is all about typing, then you should type ALL THE THINGS!!!" To haphazardly type some of the things, but not others, feels kinda arbitrary to me. On the other hand, I totally understand what you're getting at in this comment. It can be challenging to decide how much typing is the "right amount" of typing.

but I'd say about 60% of the time was spent writing Interfaces to correctly pass the right types so TS wouldn't complain, not actually writing logic

Exactly. Hence my observation (which was actually stolen from another commenter) that far too much time can be spent simply explaining - to the compiler - the code you had that was already working just fine.

Collapse
 
ydlv profile image
Yuval Dolev

I understand your frustration, however, consider just some Typescript interface. It likely says that ON interface IFoo, there is a property called x type T1 and one called y type T2, and that's it, that the interface. Now let f: IFoo. If you type f.x Typescript tells you you'd get some T1. Type f.y and Typescript will tell you you'd get some T2. Type z and.. Typescript will give you a "hold your horses". Because of course it would - what is property "z" on some IFoo? That's pretty much what Typescript is there for. Now, remember that f.x is just syntactic sugar for f["x"]. So, by specifying the interface, you're telling Typescript to limit the keys that variable f can accept, and telling it what type it returns for the allowed index keys. In Typescript, it's easy to define an interface that is like what you'd expect from "object" - as others noted in the comments. But this just isn't what this word is for, in Typescript. In Typescript, the specific type object refers to any non primitive. And of course, for when you're feeling lazy or are just indeed going very general, there's always good ol' "any".

It intrigues me that you're frustrated with that coming from C# specifically (first, because I am too, and second, because C#'s static typing is such a strength of it, and third, because I see Typescript as "Javascript translated for static-typed-OOP-languages speakers"). But each to their own I guess.

Do take time with it. Getting used to Javascript, you might just not only get used to this but also see why it's the right thing for Typescript.

BTW sorry for not writing code and formatting. Am currently not near a computer, and using phone.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

I feel like the disconnect here comes when we refer to all-purpose, utility functions. It's fine to say that, in TS, an object must be defined as having Properties X with Value Types Y, but that only works when you're trying to write a narrowly-defined function.

In the example I gave, I wanted to know if any two objects share the same key, and the same value stored in that key. In my example, I illustrated two potential "user" objects. But if you look at the code in the JS example, the function doesn't require that each object shares a common set of keys. Nor do I want to constrict the function to only comparing objects that have matching sets of keys.

IMHO, TS starts to become a major PITA when you're trying to write these kinds of utility functions - the kind of functions where you want to process any object.

And sure, you can just slap that any on the inputs, but that feels really lame. First, it's lame because any would allow the input to be, say, a number, or a Boolean. But it's also lame because this is the definition of an object from the MDN docs (developer.mozilla.org/en-US/docs/W...

Property values can be values of any type, including other objects, which enables building complex data structures. Properties are identified using key values. A key value is either a String or a Symbol value.

If that's the definition in the MDN docs, why do I need to tell TS that a string is an acceptable key for an object???

It's all fine to tightly define object data types through tools like interfaces, but when I have logic that I want to run on a wide array of objects, I shouldn't have to explain to TS that those objects could have strings for keys.

Collapse
 
psiho profile image
Mirko Vukušić

Oh I can feel your pain. Just converted my first project from js to ts and my initial feeling was exactly like that. Not just with generics but also with with "variable can be null" when it actually cannot. But relatively simple logic was not "understood" by ts which made me write extra code just to make it clear to the compiler. But I did discover some very old bugs as soon as I converted, even some of those "cannot be reproduced" hard to fix issues. On the other hands I've run into new ones, in type definitions for some libraries which work perfectly ok in js (i.e. when string or number does not matter as it is output only to HTML) but I have to add extra code for ts also to like it. Bottom line, code completion alone is worth it so I'm sticking with it. But first impression is that I dont "love it", I just have to deal with it.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Not just with generics but also with with "variable can be null" when it actually cannot.

I appreciate all of your feedback, but this particular line really struck me. I kinda ran into the same issue in my previous articles on defaultProps in React/TS. I had a situation like this:

interface Props {
   name?: string
}

const soSomething: React.FC<Props> = (props: Props) => {
   if (props.name === undefined) props.name = 'default';

   const getNameLetters = () => {
      return props.name.split('');
   }

   return <></>;
}

But TS complains about props.name.split(''); because it says that props.name could be undefined. It does this because defining the name in the interface as name?: string gives props.name a type of string | undefined.

Of course, any first-year dev can look at that code for 1 minute and realize that props.name will never be undefined. It can't be. Because if it was passed in as undefined, the logic at the top of the function will set a default value for it. But even though the first-year dev easily understands this, TS can't grasp it. So I get that ugly ol' "red squiggly" under the code.

Arrrggghhhh...

Collapse
 
psiho profile image
Mirko Vukušić

Yeah, now move that if (props.name === undefined) props.name = 'default'; down inside getNameLetters, just before return and it works! Obviously, TS expects getNameLetters() can be used in different scenarios, sometimes without setting props.name it to default. But if this is your complete app or your only call to to the func, it looks funny. Some say we cannot expect TS to get some compex login and I agree. Devs cannot either, at a glance, so it's good practice to typeguard anyway in those cases. However, some cases appear to me to be really simple and I feel I'm writing a boilerplate to make kids understand it.
However, again to defense of TS, I had the same opinion in similar case, and I was wrong :) It is similar issue but now inside async callback function. Well, value can be changed in the meantime, while async function is running. Was also complaining about writing typeguards only to discover a nasty bug for not doing so in the first place.