DEV Community

My confusions about TypeScript

Ken Bellows on January 03, 2020

I have heard endless good things about TypeScript in the last couple years, but I've never really had a chance to use it. So when I was tasked with...
Collapse
 
etampro profile image
Edward Tam • Edited

If you look closely to the problems you are having, they are pretty much all class oriented. This is exactly what I found when picking up typescript and eventually abandoned classes and shift to a more functional programming oriented coding style.

I just feel that functions are first class citizen and works better with Typescript then classes. With the way of packaging nowadays, there is not much practical you cannot do without classes.

For example, I would rather use "knex" to build the queries wrapped with functions instead of using an ORM (in your case objection) because I feel like it is more straight forward and faster to get things done, and you have more control over what you want to return.

Collapse
 
kenbellows profile image
Ken Bellows

I definitely get this perspective, and often I do prefer a functional approach over a class based approach. However, in my experience using both query builders like knex and ORMs like Objection, I find the mental mapping between my code and the database much easier to keep straight when I have a class for each table, especially the way that Objection uses static properties like idColumn and relationshipMappings to represent database constraints. Others may feel differently, but it's how my brain works.

Collapse
 
trusktr profile image
Joe Pea

This doesn't have to do with classes vs functions. If you have a place that accepts string type values, then later you're trying to pass something that is of type string | string[], that's simply wrong and TS is doing a good job telling you that this is a problem. You might know what you're passing, but the next developer, and the next one after that, won't know. This is about scaling code bases in a way that prevents accidents, classes or not.

Collapse
 
trusktr profile image
Joe Pea • Edited

To understand this more specifically, if someone write some piece of code that accepts objects of type Model based on the above class in the article, f.e. some code like

let array: Model[] = []
// push some models into the array, including Animal
// instances, then later:
const col: string = array[2].idColumn
Enter fullscreen mode Exit fullscreen mode

this code is going to fail if it unexpectedly receives string | string[] because it is expecting to have a string only, and may call string-specific methods on that string, etc.

As you can see here, this doesn't have to do with classes vs functions at all. Definitely use only functions if you like that, but this problem still needs to be taken into account, and it is simply about assignment.

Collapse
 
kenbellows profile image
Ken Bellows

@trusktr Which thing are you referencing? This is a pretty long thread at this point lol

Collapse
 
jwp profile image
John Peters • Edited

MDN explains the Class construct internals nicely. They imply the differences between a Class and Function are minimal.

With the Class, hoisting is not done and the class body is always in strict mode.

developer.mozilla.org/en-US/docs/W...

Collapse
 
etampro profile image
Edward Tam • Edited

That is exactly my point. Class in JS is just merely syntactic sugar. It introduces extra layer of complexity with virtually no gain. And that extra regconitive complexity makes it worse when you add typing on top of it.

Thread Thread
 
jwp profile image
John Peters • Edited

We could say compilers are syntatic sugar for assembly language too but compilers save time and improve quality.

The Class is not just syntactic sugar as it doesn't hoist and is always strict mode. Functions can hoist and run non strict mode.

Discounting the benefits of Typing via classes is always just subjective.

Many of us prefer Typescript for it's awesome typing support making tons of runtime errors history.

Thread Thread
 
etampro profile image
Edward Tam

Compilers in certain languages are proven to save time and improve quality (C/C++ for example) and by definition it serves the sole purpose of syntactic sugar. JS Class is arguable.

The strict mode argument is not too convincing either. It is not like we cannot run functions in strict mode. It is basically saying 'if you are afraid that you forget to write that one line of declaration, get used to always writing a class for everything'. Why not just get used to always use strict mode instead?

Thread Thread
 
jwp profile image
John Peters

As I mentioned, very little difference either way. I just don't blanketly subscribe to the 'toss class support for functions' argument. Both work exactly as billed.

Thread Thread
 
etampro profile image
Edward Tam

The difference is the unnecessary complexity which is big on terms of maintainability of code.

And I don't "blindly subscribe" to the idea. I actually tried to work with both. And coming from an OOP background it was a huge paradigm shift for me. I would suggest you to do the same and explore more.

Thread Thread
 
jwp profile image
John Peters

Saying the class is more complex is subjective.

Thread Thread
 
etampro profile image
Edward Tam

It is not subjective unless you can point out what can not be achieved without class.

Thread Thread
 
kelerchian profile image
Alan

Hi Edward, I have the same experience with your approach to use type and functions more than class for simple procedure, transforms, api-layer, and do it in a semi Data Oriented Design way.

The root of the problem is actually the by-the-book OOP usage which doesn't scale for huge projects.

I still use class though to scope different logics and contain data that seemingly has its own runtime.

Thread Thread
 
etampro profile image
Edward Tam

Interesting idea with DOD Alan, this paradigm seems interestingly common in heavy computation.

I wonder though, would you be able to achieve similar code structure with interfaces?

Thread Thread
 
kelerchian profile image
Alan

Interesting idea with DOD Alan, this paradigm seems interestingly common in heavy computation.

It is common in heavy computation. DOD is an old code structure design, it was found again on the age of PS3 if I'm not mistaken, because the machine limitation cannot match game developers ambition at that time.

I wonder though, would you be able to achieve similar code structure with interfaces?

With TypeScript's type (interface is just a syntactic sugar of TypeScript's type) and function, you can achieve a similar code structure, although for a different reason. In programs with heavy computation DoD helps with creating cache-friendly code, in the javascript environment, performance doesn't seem to increase much, but in terms of readability and extensibility, it really improves them.

Thread Thread
 
kenbellows profile image
Ken Bellows

Class in JS is just merely syntactic sugar. It introduces extra layer of complexity with virtually no gain.

I have to disagree here. While it's true that the class syntax in JS is syntactic sugar on top of functions, that sugar is very sweet. You say it's an extra layer of complexity, but that depends on where you look: it's true that it's an extra layer of syntax on top of the underlying functions and prototype chains, but in my experience class declarations are often way less complex than constructor function and prototype chain statements for the developer reading them.

These two block of code do exactly the same thing. Which is more complex?

function Animal(name, energy) {
  this.name = name
  this.energy = energy
}

Animal.prototype.eat = function(amount) {
  console.log(`${this.name} is eating.`)
  this.energy += amount
}

Animal.prototype.sleep = function(duration) {
  console.log(`${this.name} is sleeping.`)
  this.energy += duration
}

Animal.prototype.play = function(duration) {
  console.log(`${this.name} is playing.`)
  this.energy -= duration
}


function Dog(name, breed, energy) {
  Animal.call(this, name, energy)
  this.breed = breed
}

Dog.prototype = new Animal

Dog.prototype.speak = function() {
  console.log(`${this.name} says, "Woof!"`)
}

vs...

class Animal {
  constructor(name, energy) {
    this.name = name
    this.energy = energy
  }

  eat(amount) {
    console.log(`${this.name} is eating.`)
    this.energy += amount
  }

  sleep(duration) {
    console.log(`${this.name} is sleeping.`)
    this.energy += duration
  }

  play(duration) {
    console.log(`${this.name} is playing.`)
    this.energy -= duration
  }
}

class Dog extends Animal {
  constructor(name, breed, energy) {
    super(name, energy)
    this.breed = breed
  }

  speak() {
    console.log(`${this.name} says, "Woof!"`)
  }
}

I would strongly argue that the second is less complex for the developer using the code.

Unless what you meant was that classes as a paradigm are syntactic sugar that add unnecessary complexity, regardless of the language in question, in which case I have to disagree even more strongly, though that's a much longer conversation.

One more thing that's bugging me though:

John Peters: Saying the class is more complex is subjective.

Edward Tam: It is not subjective unless you can point out what can not be achieved without class.

That's just not true. Even if we suppose that classes and functions provide exactly the same capabilities and classes provide no new unique powers, the complexity that matters more in day to day life is the mental complexity for readers of the code. Some developers have an easier time thinking in functional terms and passing objects around between functions. Others (myself included) often find it far easier to think in object oriented terms, where functions are expressed as methods on objects rather than passing objects to functions. In practical terms, it's deeply subjective which paradigm is more complex.

Thread Thread
 
jwp profile image
John Peters

Thanks Ken for the elegant explanation of why some of us prefer the Class construct. Indeed it is more simple; in my mind. It promotes encapsulation in a more clear manner; to me, as well as a default strict mode.

Lots of old school JavaScript interviewers will still ask you about "hoisting" which is non-existent by default with the Class. Why do anything but use the Class? If it's not an issue any longer, why do interviewers want to test your knowledge of "hoisting" even today?

Thread Thread
 
kenbellows profile image
Ken Bellows • Edited

Thanks for the complement, now let me disagree with you 😁

Strict mode isn't something I really worry about; I write strict JS by habit at this point, and I especially don't worry about it since I've started using modules, which are also strict mode by default.

I definitely wouldn't say that hoisting is irrelevant or not an issue any more. One way I still use it all the time is when I write utility scripts. Consider this script:

// weekly-metrics.js

const fs = require('fs')
const es = require('elasticsearch')

const {sslInfo, outputFile} = processCmdLineArgs()

const client = initClient(sslInfo)
const rawMetrics = getRawMetrics(client)
const metrics = processMetrics(rawMetrics)

fs.writeFileSync(outputFile, JSON.stringify(metrics, null, 2))

////////////////

function processCmdLineArgs() {
  // ... code to process command line arguments
}

function initClient(sslInfo) {
  // ... code to initialize an ElasticSearch client
}

function getRawMetrics(client) {
  // ... code to query ElasticSearch for some metrics data
}

function processMetrics(client) {
  // ... code to process and format raw results into a custom format
}

This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available.

Tragically, I also still work on an old Angular.js 1.x project, and we do something similar to structure our Factory and Service files:

angular.module('services').factory('dataService', dataService);

function dataService($http) {
  'ngInject';

  return {
    getItem,
    getUser,
    getOrder,
    getVendor
  };

  ////////////////

  function getItem(itemId) {
    // ...
  }

  function getUser(userId) {
    // ...
  }

  function getOrder(orderId) {
    // ...
  }

  function getVendor(vendorId) {
    // ...
  }
}

In this case, the returned object at the top of the service acts as a sort of Table of Contents for the service. Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with.

So I wouldn't say that hoisting doesn't matter any more. And as you can probably tell by my examples here, I also don't think classes are the best thing for all cases. But I do love me some classes when dealing with a large amount and variety of structured data with data-type-specific functionality that can be encapsulated as class methods!

Thread Thread
 
etampro profile image
Edward Tam

Hi Ken, I will start with my appreciation for you taking the time to type in some examples for the discussion. That really helps understanding your rationale and where our disagreement come from.

Let's address the code comparison first. I think class makes it less complex only if you have to stick with inheritance. Let's take a look at the following code:

  const Animal = (name, energy) => {
    let _energy = energy
    let _name   = name

    return {
      eat(amount) {
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      sleep(duration) {
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      play(duration) {
        console.log(`${_name} is playing.`)
        _energy -= duration
      }
    }
  }

  const Dog = (name, breed, energy) => {
    let _breed = breed

    return {
      ...Animal(name, energy),
      speak() {
        console.log(`${_name} says, "Woof!"`)
      }
    }
  }

I went with a factory approach (or what Alan previously mentioned, a semi Data Oriented Design), and the code pretty much look the same as your example with class, except there is no class. However, the benefit here is that you can adopt infinite number of "traits" to your object through composition, which makes it a lot more scalable. On the flip side, you may run into situations when you need multiple inheritance in the case of classes.

Unless what you meant was that classes as a paradigm are syntactic sugar that add unnecessary complexity, regardless of the language in question, in which case I have to disagree even more strongly, though that's a much longer conversation.

This is definitely not I meant. My argument in the discussion here is specifically about classes in Typescript (even Javascript is fine by me. They don't have much gain in my opinion, but there is not much loss either so whichever way is fine).

Again, as Alan has also mentioned in a previous reply, the problem here is the by-the-look OOP. Javascript classes are not actually classes and operate quite different from other class-base languages. I think the problem was not so bad until type enforcement kicks in. You will eventually find yourself running into walls trying to apply common OOP paradigms/pattern. You writing up this post is a good example.

That's just not true. Even if we suppose that classes and functions provide exactly the same capabilities and classes provide no new unique powers, the complexity that matters more in day to day life is the mental complexity for readers of the code. Some developers have an easier time thinking in functional terms and passing objects around between functions. Others (myself included) often find it far easier to think in object oriented terms, where functions are expressed as methods on objects rather than passing objects to functions. In practical terms, it's deeply subjective which paradigm is more complex.

Again, the context here is Typescript. I am not going to claim that class in general is more complex (hell no). But you can see from the example I gave, there is not much different in outcome from a readability perspective, which is what I mean by virtually no gain. The complexity comes from Javascript's expression of classes being misleading and the headaches of trying to apply OOP on top of it.

Thread Thread
 
kenbellows profile image
Ken Bellows

That definitely clears up your point quite a lot, and there's a lot less distance between our positions than it seemed at first, so I really appreciate the clarification and your example.

I definitely agree in general with composition-over-inheritance approaches like the one you've demonstrated, especially if you have a wide variety of features that need to be combined depending on circumstance. But to be honest, I haven't really run into many circumstances IRL where it's been an issue.

To return to my specific example, I think there's a lot of value to be found in subclassing a common Model class to represent each table in my database, especially given the use fo static getters to represent metadata like which columns are the primary keys and how different tables are related. Additionally, while your example demonstrates how to simulate private instance vars using closures with the _name, _energy, and _breed vars, if you were to modify your example above to add these vars to the returned object and to add static properties to the factory functions, it just feels more and more to me like you're writing a polyfill for the class keyword. And at that point, I honestly feel that class declarations are far more readable than these sorts of factory functions, especially when you have several long-time Java devs on your team who are now coming up to speed on JavaScript (as I do) and you want to give them a graceful transition.

And that actually brings me to something else that I'd love to hear your feedback on. You said:

The complexity comes from Javascript's expression of classes being misleading and the headaches of trying to apply OOP on top of it.

This is something I've heard expressed many, many times by other devs, that JavaScript's classes are fundamentally different from classes in other languages in important ways that make them misleading to devs who transition from these other languages. But in my experience, having written classes in Java, Python, Ruby, and JavaScript (and, in case there's any doubt, as someone who has a very deep understanding of JavaScript's prototype system and how extends works under the hood), I just really haven't found that to be true, aside from the lack of certain functionality like private and protected members, which seem to be coming down the pipe anyhow.

So as someone who clearly has both a strong opinion on the subject and a better understanding of by-the-book OOP and classic design patterns than I do (admittedly, my theory suffers there), what do you see as the most important differences between classes in JavaScript and other languages, and most importantly, what do you see as misleading about them? I'm genuinely anxious to know, because I've recently entered a role where, as I mentioned, I'm training up a few long-time Java devs in the ways of the web, and I'm anxious to avoid any misconceptions.

Thread Thread
 
kelerchian profile image
Alan

This convo is fun :D

I'll throw some opinion that hopefully will help you with this.

Additionally, while your example demonstrates how to simulate private instance vars using closures with the _name, _energy, and _breed vars, if you were to modify your example above to add these vars to the returned object and to add static properties to the factory functions, it just feels more and more to me like you're writing a polyfill for the class keyword.

Java community and its derivative (Spring Boot, PHP Laravel, Ruby on Rails) to put the "smartness" of their library in the runtime bootstrap process and Objection.js is one of the libraries that walks into their path. In time this will conflict with TypeScript's approach to get types right, which is more of functional approach to type correctness (rust, Haskell, elm).

Let me take a detour a bit:

Edward's example of an object factory has a perfect type and perfect JavaScript encapsulation which is very convenient for people who cares about encapsulation and OOP in general.

  const Animal = (name, energy) => {
    let _energy = energy
    let _name   = name

    return {
      eat(amount) {               // object of type "function" is created everytime Animal is called
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      sleep(duration) {
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      play(duration) {
        console.log(`${_name} is playing.`)
        _energy -= duration
      }
    }
  }
  /* 
    Back then in my early javascript day I used this exact
    pattern to make a mini game (don't laugh), and man it was 
    damn slow. If you really have to add a method, use 
    prototype.[fnName] or use class.
  */

If you look closely to the problems you are having, they are pretty much all class oriented.

As Edward might notice, OOP's paradigm to mix data and operations in a single instance a.k.a class is where it gets problematic. And it is encouraged by languages like Java and javascript because the structure/prototype is described in the runtime, thus it can be queried a.k.a Reflection. Reflection is NOT wrong. It's a great feature, but the side effect is that it encourages the wrong approach to a problem, the storytelling.

Structures should be written like how we describe a character in a story, explicit and at once:

struct A{
 a: SomeType
 b: SomeOtherType
}

And operations should be written like how we describe a scene in a story, step by step, chronologically.

fn moveChessPiece(chessBoard: ChessBoard, chessPiece: ChessPiece, chessLocation: ChessLocation){
  chessBoard.detach(chessPiece)
  chessBoard.attach(chessPiece, chessLocation)
}

Let that new paradigm sink in. Then, let's rewrite the Animal/Dog code with some bonus code (because I really want to show you guys how this paradigm is development-scalable).

// Using type instead of class because type is zero-cost in the runtime
type Activity<ActivityTypes extends string[], Data extends object> = {
 activityType: ActivityTypes,
 activityData: Data
};

type Entity = {
 activity: Activity
};

// animal.ts
// a type based inheritance-like 
type Animal = Entity & {
 energy: number,
 name: string
};

// dog.ts
export const DogTypeSiberianHusky: unique symbol = Symbol()
export const DogTypeGoldenRetriever: unique symbol = Symbol()

type DogActivity = Activity<"sleeping" | "idle", {duration: number}>

type Dog = Animal & {
 activity: DogActivity,
 kind: "dog", // literal string
 breed: DogTypeSiberianHusky | DogTypeGoldenRetriever
}

export const DogActivityFns = {
 // create* functions are factory-ish
 createIdle: (): DogActivity => ({
   activity: "idle",
   duration: -1
 }),
 createSleep: (duration): DogActivity => ({
   activity: "sleeping",
   duration
 }),
 isExpired: (activity: DogActivity) => activity.duration <= 0,
 canSleep: (activity: DogActivity) => activity.type === "idle"
}

export const DogFns = {
 // called on render on every entity instantiated in world
 onUpdate: (dog: Dog) => {
  // assumptions are Dog as an Entity is a mutable object
  // meaning in this scenario dog object are not recreated { ...dog }
  // for the sake of performance
  const { activity } = dog
  switch(activity.type){
    case "sleeping": {
     if(activity.type === "sleeping"){
      activity.duration -= 1
      if(DogActivityFns.isExpired(activity)) {
       dog.activity = DogActivityFns.createIdle(activity)
      }
     }
     return
    }
  }
},

 triggerSleep: (dog: Dog, duration: number) => {
  if (!DogActivityHelper.canSleep(dog.activity)) return
  dog.activity = DogActivityHelper.createSleep(duration)
 }
}

// And then somewhere on the code
// There's an event listener that can be fired outside of the game's main loop

activityEventTrigger.subscribe("triggerSleep", (dog, duration) => 
  DogFns.triggerSleep(dog, duration)
)

The code above shows a really easy way to get a pretty complex idea done in a simple manner, only by separating operations and structure as opposed to the classical OOP where method and property is in one place so that it's confusing whether we should write attack() or receiveDamage().

Let's go back to the case of ObjectionJS, we've detoured pretty far.

TypeScript clearly doesn't do much good to the usage of ObjectionJS as it's very complex in terms of type correctness (e.g. idColumn could be string or string[]).

I'd treat ObjectionJS modules as another "unknown territory" in the application, like localStorage.get, fetch, fs.readFileSync, redisClient.get, etc. Let it be functions that return unknown. You could practically give unknown or if not possible any (forgive me, lord) to the idColumn and other static method return type.

Now create a funnel function where it returns that unknown object as a result of ObjectionJS query with a precise type or return an error if it is not the expected result.

async function fetchUserFunnel(){
  const raw = await fetchUserWithObjectionJS();
  if (!User.is(raw)) {  // decode process, will explain later
    return { value: null, error: new DecodeError() };
  }
  return { value: raw, error: null };
}

To scale this pattern let's make a function factory a.k.a higher-order function for it. I'll be using io-ts for the parser to demonstrate the new paradigm. Also, check out io-ts if you haven't, it's a cool lib.


export class DecodeError extends Error {}

// the hof
function makeFunnel<T extends object>(
  validateFn: (t: unknown) => t is T
) {
  return (raw: unknown): {value: T, error: null} | {value:null, error: DecodeError} => {
    if(!validateFn(raw)){
      return { value: null, error: new DecodeError() };
    }
    return { value: raw, error: null }
  }
}

// the usage
export const UserCodec = t.type({
  userId: t.string,
  userName: t.union([
    t.string,
    t.null
  ]),
})
export const UserFromDB = t.intersection([
  User,
  t.type({ children: t.array(UserCodec) }),
]);
export type UserFromDB = t.TypeOf<typeof UserFromDB>;
export const funnelUser = makeFunnel((t: unknown) => UserFromDB.is(t))
export const fetchUserById = 
  (id: string) => 
    User.query()
      .where("id", id)
      .withGraphFetched('children')
      .then(result => funnelUser(result[0]))

And using the fetch function would be

// And how to use it would be
const result = await fetchUserById(query.id)
if(result.error) return res.send(500); // corrupted data in the database, data is not contractual

return res.send(result.value)

If you're really sure that your database will always return the correct schema, do this, but I don't recommend this.

const users = await User.query()
  .where("id", id)
  .withGraphFetched('children') as UserFromDB

The code is long and it's already the optimal amount of code to achieve type correctness in your application.

I guess enforcing strict TypeScript in the ObjectionJS is not the best angle to approach this problem. ObjectionJS is of a different paradigm, and let it be that way because we can't change how it behaves nor we can put a strict TypeScript rule to it.

Let ObjectionJS be unknown. As a substitute, enforce type correctness in the business logic, detached from the resource facing layer.

Thread Thread
 
jwp profile image
John Peters

Ken;
You had asked "How would you restructure that program to use a class?" with respect to the goodness of "hoisting".

If multiple components need to use something in common, I usually abstract the component to an Angular Service today. It gives me that ability to re-use anything anywhere. The only drawback is that I have to import the service in order to reuse the code within it.

Thread Thread
 
kenbellows profile image
Ken Bellows

@Alan, thanks for the in-depth response! I'll have to take some time to read through it and consider it. Interestingly, I notice that Objection.js actually provides typings, though they seem rather convoluted and maybe more intended for internal library use; what do you think? github.com/Vincit/objection.js/blo...

@john Peters, sure, I think that's a great approach, and personally I don't consider an extra import a drawback at all; if anything, I prefer smaller individual modules, so I'm happy to ad an extra import in exchange for a smaller overall file 😁

Thread Thread
 
kelerchian profile image
Alan

@Ken I would take the shortest, easiest, and safest path that scales both in performance and development. TypeScript strict typing is relieving me from constantly fearing "undefined is not a function" issue, but if objection typing turns out to be a burden more than a help to my team, I would consider excluding objection from the strict typing in favor of development efficiency. It's just my opinion though, you know the lib better and might prove me wrong on this.

 
etampro profile image
Edward Tam
To return to my specific example, I think there's a lot of value to be found in subclassing a common Model class to represent each table in my database, especially given the use fo static getters to represent metadata like which columns are the primary keys and how different tables are related.

If you find value in using subclassing in your case, then go for it. I am not going to pretend to be the know-it-all :)

having written classes in Java, Python, Ruby, and JavaScript ...

So we pretty much have the same background :)

what do you see as the most important differences between classes in JavaScript and other languages, and most importantly, what do you see as misleading about them?

I think my biggest complaint about JS classes is the encapsulation. Let's see the following code as example:

class Person {
  constructor (name) {
    this.name = name
  }

  talk () {
    console.log(`${this.name} says hello`)
  }
}

const person = new Person('Tom')
const mockElement = {}

mockElement.onClick = person.talk
mockElement.onClick() // this.name -> undefined!

As you can see person.talk is not encapsulated as a part of the class Person. Instead, the context is related to where it is called instead. This problem is especially painful when you work with frontend code (e.g. React). You will find the context being mutable, and will be forced to bind them everywhere. So as Alan also mentioned in the other reply, I tend to go with functions and closures in order to workaround this hustle.

Thread Thread
 
kenbellows profile image
Ken Bellows

Ah I see. Yeah that's definitely a frustration sometimes, and I've dealt with the React issues you mention. That said though, I wouldn't tie that to JavaScript's class system at all; that's just how objects in general work in JavaScript, and it has its pros and cons. While it's true that it can cause some issues, it also facilitates the composition-over-inheritance style you mentioned above, as it lets you copy methods from one object directly to another without any fancy footwork. Pros and cons, I guess

Thread Thread
 
jwp profile image
John Peters

Interesting for sure! I have never seen this before.

Class Encapsulation seems to imply the name property only for the class object. So the behavior you've shown would be expected because mockElement.onClick is not a Person object. Right?

Indeed changing the assignment to const mockElement = this.person gives proper context when calling mockElement.Talk();

It looks to me like the class object's context is not mutable. That's a good thing.

Thread Thread
 
etampro profile image
Edward Tam
It looks to me like the class object's context is not mutable

Actually, class object's context is mutable. Let's take a closer look at the example again:

class Person {
  constructor (name) {
    this.name = name
  }

  talk () {
    console.log(`${this.name} says hello`)
  }
}

const person = new Person('Tom')
const mockElement = {}

mockElement.onClick = person.talk
mockElement.onClick() // this.name -> undefined!

mockElement.conClick = person.talk.bind(person)
mockElement.onClick() // this.name == 'Tom'

You can actually manipulate the reference of this. This is what makes it misleading in an OOP perspective because you would have thought the context is part of the encapsulation in the definition. The problem is not strictly coming from javascript classes, but more from the discrepancy of the expected behaviour of how class should be vs how javascript class actually is.

Thread Thread
 
kenbellows profile image
Ken Bellows

But I really don't think it has anything to do with classes. I think a better statement would be, "The problem is coming from the discrepancy of the expected behavior of how objects should be vs how javascript objects actually are". IMHO, the confusion is identical using a factory:

const Person = (name) => {
    return {
        name,
        talk(amount) {
            console.log(`${this.name} says hello`)
        }
    }
}

const person = Person('Tom')
const mockElement = {}

mockElement.onClick = person.talk
mockElement.onClick() // this.name -> undefined!

mockElement.conClick = person.talk.bind(person)
mockElement.onClick() // this.name == 'Tom'

I can't see this example being any less confusing than the class example just because we don't use the new keyword. The confusion all boils down to this step:

mockElement.onClick = person.talk
mockElement.onClick() // this.name -> undefined!

Regardless of how we built the person object, what's confusing is that the talk method loses its this context when attached to something else.

Now of course, one way to solve this problem is to use purely private vars and closures like you did in your Animal example, but personally, I have one really big problem with that approach: it makes the properties themselves inaccessible. You can no longer do doggo.name = 'Fido' to rename your dog. And hey, If all you need is private vars, go for it, but I don't think this approach covers all cases, or even most.

You can, of course, use a getter and a setter for each public property to make them accessible while keeping the closure and its advantages, but at that point the complexity of the code really ramps up while the readability falls, and personally, I just don't know if it's worth the trade-off:

const Animal = (name, energy) => {
    let _energy = energy
    let _name   = name

    return {
      get energy() {
        return _energy
      },
      set energy(energy) {
        _energy = energy
      },

      get name() {
        return _name
      },
      set name(name) {
        _name = name
      },

      eat(amount) {
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      sleep(duration) {
        console.log(`${_name} is eating.`)
        _energy += amount
      },

      play(duration) {
        console.log(`${_name} is playing.`)
        _energy -= duration
      }
    }
  }

  const Dog = (name, breed, energy) => {
    let _breed = breed

    return {
      ...Animal(name, energy),

      get breed() {
        return _breed
      },
      set breed(breed) {
        _breed = breed
      },

      speak() {
        console.log(`${_name} says, "Woof!"`)
      }
    }
  }

That up there feels like a lot of boilerplate to produce an object with three properties, just so I can occasionally write myBtn.click = myDoggo.speak instead of myBtn.click = () => myDoggo.speak().

This is definitely a personal preference, but I don't think the relatively minor tradeoff of context-free methods is worth it. I personally don't use them nearly often enough to justify that kind of a change across the board. If you do, hey, maybe it's for you, but I personally am so used to JavaScript objects and how functions and this work that it's barely even a frustration, and tbh I just really love the elegance of the class syntax. Unpopular opinion, but IMO it will be even better once the class field and private class field syntaxes become standard.

Thread Thread
 
etampro profile image
Edward Tam
I think a better statement would be, "The problem is coming from the discrepancy of the expected behavior of how objects should be vs how javascript objects actually are".

I think that is a fair statement. Regardless, that was fun discussion and I think I learnt something from it :)

Thread Thread
 
kenbellows profile image
Ken Bellows

Definitely 😁 Thanks to everyone in this thread for the back and forth, it was a good discussion and we made it out without any flames

 
jwp profile image
John Peters

Ken, thanks for the banter;

"This is the sort of layout I like to use for my scripts because I can open up the file and immediately see an outline of the main steps of the script, then ctrl+click my way into whichever part I need to read or mess with at the moment. This works because of hoisting. All the functions for individual steps are hoisted to the top of the file so they're immediately available."

"I can immediately see the outline"...

Do you mean at the program layer? Is this really different than a class construct where as you pointed out the syntax doesn't require the function keyword?

"ctrl+click my way into whichever part I need to read or mess with at the moment"

From the command line of an IDE? Or in browser console/debug mode?

"Anyone who opens that service knows immediately what methods it provides, and again, they can ctrl+click directly to the one they're concerned with."

I hated AngularJs for the inability to get Service's to expose their properties and functions using Intellisense/Autocomplete. I think you are showing something that worked for AngularJs?

"So I wouldn't say that hoisting doesn't matter any more."

How would you define Hoisting and when to use it over other constructs?

Thread Thread
 
kenbellows profile image
Ken Bellows

Do you mean at the program layer? Is this really different than a class construct where as you pointed out the syntax doesn't require the function keyword?

I do mean at the program layer, yes. As far as whether it's really different than a class, I mean... yeah, it is. How would you restructure that program to use a class? Unless you want to go the Java route and use a Main class:

class Main {
  constructor() {
    const {sslInfo, outputFile} = this.processCmdLineArgs()

    const client = this.initClient(sslInfo)
    const rawMetrics = this.getRawMetrics(client)
    const metrics = this.processMetrics(rawMetrics)

    fs.writeFileSync(outputFile, JSON.stringify(metrics, null, 2))
  }

  processCmdLineArgs() {
    // ...
  }

  initClient(sslInfo) {
    // ...
  }

  getRawMetrics(client) {
    // ...
  }

  processMetrics(rawMetrics) {
    // ...
  }
}

// run Main
new Main()

If that's what you mean by using a class construct, then we just have very different preferences, because that's a nightmare to me.

But even if that's your personal preference, the point is that I still see plenty of code written this way by other developers, and it doesn't make sense unless you understand that function declarations are hoisted. My comment was mostly in response to when you said:

Lots of old school JavaScript interviewers will still ask you about "hoisting" which is non-existent by default with the Class. Why do anything but use the Class? If it's not an issue any longer, why do interviewers want to test your knowledge of "hoisting" even today?

My point is that hoisting is still a fundamental aspect of JavaScript and it's pretty important to understand, or at least be aware of, even if you have to look it up from time to time. Not understanding hoisting can lead to some subtle, hard-to-find bugs; I've been there a few times.

"ctrl+click my way into whichever part I need to read or mess with at the moment"

From the command line of an IDE? Or in browser console/debug mode?

Either, I suppose, though I was mostly thinking of an IDE context. My point is that anyone reading my code (including myself) can open the file and see the overview of the file, then quickly jump to the relevant function using standard IDE tools. And IIRC, ctrl+click works in browser dev tools' "Sources"/"Debug" tabs as well.

I hated AngularJs for the inability to get Service's to expose their properties and functions using Intellisense/Autocomplete. I think you are showing something that worked for AngularJs?

Totally agree, my biggest frustration with AngularJS is the total lack of intellisense. And no, unfortunately, this doesn't help with intellisense; in fact, it's more important in cases where intellisense doesn't work. The point is that when you see some code saying, dataService.getVendor(id), you can jump over to data-service.js, easily see the available methods, and ctrl+click stright to getVendor() without needing ctrl+f or other mechanisms. It's even more useful in a service that uses its own methods internally, since ctrl+f is less useful in that case.

How would you define Hoisting and when to use it over other constructs?

Hoisting is a step in the interpretation of a JavScript scope wherein all declarations are basically plucked out of their place in the script and moved to the top of the scope. It's not really something you use or don't use, it's just the way JavaScript works.

It's important to note, however, that declarations are hoisted, but initializations are not. So for example, in this code:

(function do() {
  console.log(x)
  var x = 10
})()
// Output:
// => undefined

You'll get undefined as your output. However, function declarations are special because they don't have separate declaration and initialization steps, it's all-in-one, so they get hoisted completely to the top:

(function do() {
  console.log(x)
  function x() { console.log('hey!') }
})()
// Output:
// => function x() { console.log('hey!') }

And const and let are a bit weirder. They do get hoisted, but there's this weird thing called the Temporal Dead Zone where they're defined but not initialized and you'll still get errors if you try to reference them... so a lot of people say they "aren't hoisted", which is technically not true, but it might as well be.

Hopefully that all made sense... it's a weird bit of rather esoteric JavaScript knowledge that you can basically look up when you need it.

Thread Thread
 
jwp profile image
John Peters

Ken thank you for spending so much time on this. Definitely something for me to study.

Collapse
 
koresar profile image
Vasyl Boroviak

Thanks for the amazing quote! I copied to my twitter. Hope it's OK.

twitter.com/kore_sar/status/121395...

Collapse
 
etampro profile image
Edward Tam

No problem!

Collapse
 
nickytonline profile image
Nick Taylor

For the first one

function handleRelations(modelClass: extends Model) ...

here is a very simple TypeScript Playground example example.

TLDR;

class Model {
    // Some awesome code
}

function handleRelations(modelClass: Model) {
   // do something with modelClass
}

In terms of readability for

function converter(input: Map<{ [key: string] : any }, { [key: string] : any }>): Map<{ [key: string] : any }, { [key: string] : any }>

see this TypeScript Playground example

TLDR;

function converter(input: Map<{ [key: string]: any }, { [key: string]: any }>): Map<{ [key: string]: any }, { [key: string]: any }> {
    // awesome code
    return input;
}

// can become

function converterWithBetterTypes(input: Map<Record<string, any>, Record<string, any>>): Map<Record<string, any>, Record<string, any>> {
    // Note: Record<T> is a built-in TypeScript type
    // awesome code
    return input;
}

// can become
// Probably best not to use any
type ConverterShape<T extends any = any> = Map<Record<string, T>, Record<string, T>>

function converterWithReadableTypes(input: ConverterShape): ConverterShape {
    // awesome code
    return input;
}

When I have a chance, I'll check out the other issues you're having.

Collapse
 
kenbellows profile image
Ken Bellows • Edited

Huh, Record<T> is a pretty handy trick. Definitely nicer than the bare syntax. And yeah, in cases where it gets too complicated I definitely do declare little types, but it seems like a lot of overhead to have to declare a local type for every function that uses Maps or Sets of objects like that... I'll have to think about that one.

Regarding passing around classes and subclasses, in your example, modelClass: Model is still and instance of some class extending Model, rather than a class itself. I need to reference the classes themselves to get at static methods and properties. Here's a playground example (in a broken state) showing what I mean.

Thanks for the responses!

Collapse
 
nickytonline profile image
Nick Taylor

Ahh, I see what you mean. My bad. You can do this

TLDR;

class Model {
    // Some awesome code
}

function handleRelations(modelClass: typeof Model) {
    return modelClass;
}

Does that handle your use case?

Thread Thread
 
kenbellows profile image
Ken Bellows

Oh man yes it does, that's exactly what I was looking for! I didn't realize that was a thing! Thanks!

Thread Thread
 
coly010 profile image
Colum Ferry

Or even better using generics:

function handleRelations<T extends Model>(modelClass: T) {
    return modelClass;
}
Thread Thread
 
nickytonline profile image
Nick Taylor

Generics for sure, but it still needs to be typeof Model as he doesn’t want an instance of the class.

Thread Thread
 
coly010 profile image
Colum Ferry

I missed that. In that case, he shouldn't use a class here, rather:

export type Model = {...}

// Or

export interface Model {...}
Thread Thread
 
nickytonline profile image
Nick Taylor • Edited

I rarely use classes aside from React components (pre-hooks) as I prefer functions even though I'm not a full FP kind of person, but they are still used by many. 😉

For sure, a type is an option. It just depends on what he wants to do in that function. If an interface or type is used and he wanted to create an instance of the Model class, using an interface or type would not work. If he wanted to use it for duck typing in his function, then an interface or type would suffice.

interface SomeModelInterface {
    someMethod: () => boolean;
}

class Model implements SomeModelInterface {
    someMethod() {
        return true;
    }
}

function handleRelations(modelClass: typeof Model) {
    const instance = new modelClass();
    const value = instance.someMethod(); // true;
}

function handleRelations2(modelClass: SomeModelInterface) {
    const instance = new modelClass(); // errors
    const value = modelClass.someMethod(); // All good
}

TypeScript playground snapshot

Here's a sample TypeScript Playground for those interested.

Thread Thread
 
coly010 profile image
Colum Ferry

Very interesting. I didn't realise you could combine new with typeof T params.

Thread Thread
 
kenbellows profile image
Ken Bellows

Yeah Model isn't my class, it's provided by Objection.js, an ORM library I'm using. As mentioned in the post, I need to access static properties of various subclasses of Model, so I need to pass around the classes themselves.

Collapse
 
ronnewcomb profile image
Ron Newcomb • Edited

1)

function handleRelations<T extends Model>(modelClass: T) {

Look up Generics Constraints in the typescript manual.

2) That's not a Typecript question, that's an object-oriented question. The answer is "you can't, and shouldn't".

If I have an Animal[] and .map it to call a function on each return value...
animals.map(a => a.idColumn().whatIsValidHere)
... then what is always safe for whatisvalidhere if it's sometimes a string sometimes an array sometimes an Invoice, etc.

3)

interface SimpleObject<T> {
  [key: string] : T
}

is a bit more useful. Stick in /shared/ or wherever you keep such things.

Collapse
 
jdforsythe profile image
Jeremy Forsythe

You beat me to it! typeof Model and T extends Model are the useful features here, but I also agree with Edward Tam - you should probably eschew subclassing in general. "Is a" object relationships are the tightest form of coupling that exists. Instead use functions or even mixins, if you must attach "methods" to your objects. You probably don't need a base class when you have interfaces and mixins available to you.

Collapse
 
coly010 profile image
Colum Ferry

is a is strong, but can be loosened with Factories and Bridge Pattern

Thread Thread
 
jdforsythe profile image
Jeremy Forsythe

Haven't found a good use for the Bridge Pattern, but I've often wondered why more ORMs aren't using Factories/mixins

Collapse
 
kelerchian profile image
Alan

JS lover too until I met TS here. I've led a big full-stack app project using TypeScript so I probably can give you some insight.

I agree with this comment by Edward Tam. I suggest not to rely much on class and class extensions as TypeScript doesn't have good support for OOP extensions. Even though TS was born from OOP paradigm, it's actually easier to just use type and namespaced-functions is the form of static methods in a class. I use class only if I need a long-running data scope which seemingly has "a separate runtime" and I use it rarely.

I would recommend watching this youtube.com/watch?v=yy8jQgmhbAU and youtube.com/watch?v=aKLntZcp27M. Even though it's rust and c++ it gave a lot of insight on how to make code that is development-scalable, meaning adding/changing/removing features in the future will not be as hard as when you use by the book OOP, which I mistakenly use in my first projects.

Also, check this out, TypeScript 3.5's feature higher-order type inference which is super neat.

It's nice to see more people embracing TypeScript. Welcome to the ecosystem, good luck and have fun.

Collapse
 
mattgson profile image
Matt

Out of interest, do you use an ORM with TS? If so which one? I have yet to find one that doesn't seem like it wants to work strictly in the OOP paradigm. I need an ORM that properly supports relational data structures like ternary relationships. The only one that looks bearable is Objection which essentially seems to be a query builder that holds a representation of your schema in code to provide helpers to do easy queries and eager loading etc. Unfortunately as you've pointed out, it doesn't really have support for TS.

Collapse
 
kenbellows profile image
Ken Bellows

So, I wrote this article a couple months ago now, and since then I've developed my project into a pretty sizable codebase, still using Objection with TypeScript. IMHO, it works fine. I understand what a few other commenters have said about TypeScript and classes, but after digging in a bit deeper, I haven't encountered any problems I couldn't pretty easily work around. I like Objection a lot; I think it occupies a pretty nice middle ground between query builders and ORMs where it gives you some very nice class-based features out of the box, but still provides low-level access to SQL queries when you need it in a way that many Hibernate-style ORMs do not.

Collapse
 
jethrolarson profile image
Jethro Larson

It can help to be able to ask quick questions on gitter.im/Microsoft/TypeScript or launchpass.com/fullstacktypescript

Collapse
 
kenbellows profile image
Ken Bellows

Awesome, thanks! I'll keep those groups handy

Collapse
 
abdullahdibas profile image
Abdullah Di'bas

Making this a typed language you need to expect that it makes more limitations on the code you write to help you avoid any bugs or errors at runtime. I think the first two points you mentioned are designed in Typescript to help you code in a way that doesn't violate Liskov Substitution Principle which is one of the SOLID design principles.
en.m.wikipedia.org/wiki/Liskov_sub...

Collapse
 
kenbellows profile image
Ken Bellows

The second point, absolutely. Not the first point though; what I was reaching for but couldn't find was typeof Model, as in function foo(model: typeof Model).

Collapse
 
coly010 profile image
Colum Ferry • Edited

This works, but it's rather annoying to have to reinvent the wheel this way

Interfaces are not for reinventing the wheel.
Your interfaces should be defined first and should contain the minimum common behaviour between similar objects that will implement them.

Think of an interface as a contract that needs to be fulfilled by the classes that implement it.

This means any method that only needs to use a specific behaviour that multiple objects contain, only needs to accept any object that implements it.

interface IExecutable {
    execute(): void;
}

class LogExecuter implements IExecutable {

    output: string;

    addOuput(log: string) {
        this.output += log;
    }

    execute(){
        console.log(this.output);
    }

}


class FileExecutable implements IExecutable {

    writeToFile() {
       // Do file ops
    }

    execute() {
        this.writeToFile();
    }


}

myMethod(executable: IExecutable) {
    executable.execute();
}

// Now we can call myMethod with either of the two classes above

myMethod(new LogExecuter());
Collapse
 
kenbellows profile image
Ken Bellows

The point is that the base classes are already defined in the library I'm using, and I was only using the interface as a hack to be able to pass around the classes in the way I needed to. I understand the purpose and usefulness of interfaces as a construct, but that wasn't my situation.

Collapse
 
michaeljota profile image
Michael De Abreu

I think the issue when you try to change the signature of a subclass, is that any subclass should be assignable to their superclass.

See in the example: Playground

You can run it and see the error. I understand that in your point of view you could perfectly handle that case, but that's a sample use case.

Collapse
 
jwp profile image
John Peters • Edited

Very good question. When moving to Typescript and the concept of the Classes and ability to extend a base class, one rule must be absolutely followed. The is-a relationship is paramount. The parent must be a type of base.

Base classes don't really return anything (other than hosting properties and funtions) rather, they morph all things public to be a part of the parent.

This is where intellisense shines because no API is needed as the editor discovers all props and functions as you type.

Collapse
 
jwp profile image
John Peters • Edited

Interface defintions are not strictly needed. They only provide the abilty for other users to inject their own implementation of a pattern. This stops the need to always use just one concrete class and is favored mostly by testers.

Collapse
 
jwp profile image
John Peters • Edited

In your example you do not need to override the getter for Id. Why? Because it's in the base class. When referring to static properties one must call out the class that contains them.

If each of the static methods are to return the same thing you only need one in the base class. DRY is good.

Collapse
 
kenbellows profile image
Ken Bellows

Each class's idColumn getter returns a different thing, as shown in the example code. That's why I was overriding them.

Collapse
 
coly010 profile image
Colum Ferry
interface SimpleObject {
  [key: string] : any
}

Instead of this you can just use the object type?

myFunc(simpleObj: object): object {...}
Collapse
 
jhoobergs profile image
Jesse Hoobergs

Not sure if someone mentioned it, but do you know about the 'object' (typescriptlang.org/docs/handbook/b...) type?

Collapse
 
kristijanfistrek profile image
KristijanFištrek

I am dealing with Typescript for a while now but this sure demystified certain things ^ pretty cool!

Collapse
 
jwp profile image
John Peters • Edited

Change the string or string[] returns to type of any. To fix complaints. Just put in checks when used.

Collapse
 
jdforsythe profile image
Jeremy Forsythe

We disallow type any in our code. It removes any benefits you get from using a typed language