#How do I use typescript in ES5 classes?

103 messages · Page 1 of 1 (latest)

strong crest
#

Normally you would use the keyword class to create a class in JS as

class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
}

var car = new Car('Nissan', 'Sunny');

But I'm using ES5 to declare classes as


function Car(make, model) {
  this.make = make;
  this.model = model;
}

var car = new Car('Nissan', 'Sunny');

My problem is that I get a Typescript error for this

'this' implicitly has type 'any' because it does not have a type annotation.

so I add


function Car<T>(this:T, make:any, model:any) {
  this.make = make;
  this.model = model;
}

but it gives me another error for this.make
Property 'make' does not exist on type 'T'.

When I add any to this

function Car(this:any, make:any, model:any) {
  this.make = make;
  this.model = model;
}

It goes away, but I'm not sure if using any is the right choice. I need some advice in this.

sudden summit
#

Well I don't think you're gonna be passing in the shape of car when making a car

#
type Car = {
    make: unknown
    model: unknown
}

function Car(this: Car, make: unknown, model: unknown) {
  this.make = make;
  this.model = model;
}
#

Don't use any please... even if it's just a demo

#
type Car = {
    make: unknown
    model: unknown
}

const Car = function Car(this: Car, make: unknown, model: unknown) {
  this.make = make;
  this.model = model;
} as Function as new (make: unknown, model: unknown) => Car

const car = new Car('foo', 'bar')
#

This add a cast to function to give it a construct signature so you can new it.

#

It might be possible to make the types cleaner, I don't use this often enough to know.

strong crest
sudden summit
#

as far as I know yeah, TS doesn't seriously support the pattern. That set there might be some way that I don't know about

#

I'd look at some way to avoid duplication like re-using types

#
type Car = {
    make: unknown
    model: unknown
}

type CarArgs = [make: unknown, model: unknown]

const Car = function Car(this: Car, ...args: CarArgs) {
  this.make = args[0];
  this.model = args[1];
} as Function as new ( ...args: CarArgs) => Car

const car = new Car('foo', 'bar')

You could create Car based on Args or vice versa, but that may not be useful, depending on use case. Not sure this approach is any better really

#
type Car = {
    make: unknown
    model: unknown
}

function CarConstructor(this: Car, make: unknown, model: unknown) {
  this.make = make;
  this.model = model;
}

const Car = CarConstructor as Function as new ( ...args: Parameters<typeof CarConstructor>) => Car

const car = new Car('foo', 'bar')
#

How about this one?

#

That issues is from 2015. So really the answer if you want better support is to update to classes. Not sure what circumstances lead you to want to work with constructor functions.

craggy beacon
#

Consider also, not using classes at all and doing things with structural types and free functions

#

I’ve found that this is almost always cleaner overall

#

Test infrastructure is one of the only cases that makes me look towards classes, and even then I don’t prefer it

strong crest
craggy beacon
#

I can’t remember the last time I felt I needed a ‘new’ keyword in TS

#

You can always just write a factory

#

Even when I’m in a language like Java that forces you to frame everything in terms of classes, I generally try to sugar away ‘new’ whenever possible; typescript lets you just not bother with it in the first place

strong crest
# craggy beacon You can always just write a factory

I think I'll take the factory function route, seems the one to present the least issues in the future. If I can pick your brain, I'm trying to do a simple factory function to understand how they work. How can you use this inside of them? This is an example that I came up with, pick it apart if you can:

type Car = {
    brand: string
}

function Car(this:Car, brand:string){
    return {
        brand,
        whatBrand: `My brand is ${this.brand}`
    }
}

const Ford = Car("Mustang")
sudden summit
#

You can't use this

#
type Car = {
    brand: string
    whatBrand: string
}

function Car(brand: string): Car {
    return {
        brand,
        whatBrand: `My brand is ${brand}`
    }
}

const Ford = Car("Mustang")
strong crest
# sudden summit You can't use `this`

According to this article you can definitely use this. As displayed:

function enemyFactory(type, health, attackPower, speed) {
    return {
        type: type,
        health: health,
        attackPower: attackPower,
        speed: speed,
        attack() {
            console.log(`${this.type} attacks with ${this.attackPower} power!`);
        },
        move() {
            console.log(`${this.type} moves at a speed of ${this.speed}.`);
        }
    };
}
Medium

When I started web development about five years ago, I primarily worked with Ruby. And when I was learning Ruby, I picked up object…

sudden summit
#

"Factory function" is sometimes used to refer to what I call a Constructor function, i.e. the original pattern.

#

If you function is called without new then it doesn't have a this, unless it's an arrow function, in which case it has any this that exists in the surrounding scope, but that is more for use in classes

#

--
Oh sorry
in that case he is using this in methods on that object. You can do that.

#
const o = {
    a: 1,
    f() {
        console.log(this.a)
    }
}
o.f()
#

this code will log 1

#

If you call o.f() this in f will be set to o

#

if you do:

const f = o.f
f()

then this will be null/undefined

#

but you can also do

const f = o.f.bind(o)
f()

and now this is o again

#

the way that methods can lose their parent is a bit iffy

strong crest
sudden summit
#

Why bind the global?

#

If you use an arrow function inside a class, then it catches the this from the context and can never lose it. f here works like above, but g will always have the instance of C as this. The disadvantage is that each C has a new g function instead of sharing the same one - this doesn't really matter unless you are making many instances and care a lot about performance.

class C {
    a = 1
    f() {
        console.log(this.a)
    }
    g = () => {
        console.log(this.a)
    }
}
#

I use classes and arrow functions personally. Works well for me.

craggy beacon
#

Arrow functions are the solution here.

#

I only use the function keyword in global scope. Anything in an object is an arrow function

#

I hate javascript’s dynamic ‘this’ behavior and this convention avoids it entirely and makes things work predictably

sudden summit
#

Can you use arrow functions with simple objects easily?

craggy beacon
#

Yes

#

A member that is an arrow function works just fine in basically all cases

naive fossilBOT
#
const o = {
    a: 1,
    f: () => {
        console.log(this.a)
//                  ^^^^ ^
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
    }
}

o.f()
sudden summit
#

doesn't seem to want to attach to the object, because it's looking for a this in current scope

craggy beacon
#

You don’t need ‘this’ unless it’s already there in the ambient scope; you can capture other stuff normally

#

Anything else in the object that you need can be a regular capture

sudden summit
#

Yeah but I'm saying that to add a method to a simple object it needs to be a method not an arrow function, because you don't have a this for the arrow function

#

And if you want to protect it from being separated from the object, then you need to do binding

craggy beacon
#

You don’t need a ‘this’ if it’s not a class, though. If it’s a normal object it’s just holding references that you can capture

sudden summit
#

?

#

What about when you want to add a method that accesses a field on the object?

craggy beacon
#

Capture the field on the object

sudden summit
#

I don't know what this means

#
const o = {
    a: 1,
    f() {
        console.log(this.a)
    }
}
o.a = 2
o.f()
craggy beacon
#

In whatever function is spitting the object out you can construct the member function to have a reference to whatever it needs before adding it to the object

sudden summit
#

No you can't

#

You can't reference o.a here without a reference to this

#

because it can change

#

if it's static then sure it's OK

#
const o = {
    a: 1,
    f() {
        console.log(this.a)
    }
}
o.f = o.f.bind(o)
#

this seems to be the cleanest way to write it without any binding issues

craggy beacon
#
const o = {
  a: 5,
  func: () => {
    o.a = 7;
  }
}```
#

what is wrong with this?

#

my understanding has been that classes are only really useful for subclassing; none of the reference or construction behavior is particularly indispensable

#

and subclassing is a weird rigid dependency that I almost never find I need to actually do

#

constructors have enough weird edge-cases that I just, avoid them in general in favor of plain old object-producing functions

sudden summit
#

Cool

craggy beacon
#

yes

#

try it out

sudden summit
#

Had no clue haha

craggy beacon
#

this is also how you bind listeners that unbind themselves

#

you reference the thing you're declaring from the body of the declaration

#

JS allowing this is really nice

sudden summit
#

So if you're returning an object, you just assign it to a variable before you return so you can reference it?

craggy beacon
#

yeah

#

given that JS can do this i have never really understood JS classes

#

i just never write them

#

they feel like "we added these to make this familiar to java programmers"

#

and not "we added these because the language needed them"

sudden summit
#

Well, I think classes are better than constructor functions

#

Probably constructor funcs should never have been added

craggy beacon
#

i think constructors as a language feature are just, a general mistake

#

they're bad

#

regardless of language

#

Rust has the right idea with getting rid of them entirely

#

(doesn't Smalltalk do that, too?)

sudden summit
#

Classes kind of make sense as a better way to do constructor funcs. But yeah maybe not needed. I came from PHP and some C++, so I fit into the "it's familiar" camp. I use them because I've not found a reason not to.

#

And I like the sound of new Thing rather than makeThing

craggy beacon
#

you can just name the function thing

#

the only really useful feature of classes to me, is the automatic generation of a namespace

#

if it were easier to make sub-namespaces then you could do Thing.create

#

which is way better than new to my eyes

meager stag
#

Yeah, @strong crest if you want you can do plain objects returned from plain functions and not involve this at all.

#

Or if you're going to do classes, then I'd just do class syntax. It's far better supported by Typescript and there aren't really any downsides to it compared to the ES5 function approach.

#

We used the old ES5 class style, but migrated them to ES6 classes when we migrated our codebase to Typescript and really didn't have any issues.

#

Seems that classes are hard to debug and bring many other issues to the table.
I haven't found this to be true and especially not if your point of comparison is ES5 classes.

#

People have arguments against class in general, but not really any I'm aware that are solved by using the old school syntax (unless maybe if you're really into stuff like mixins or the weirder corners of prototypical inheritance).

#

And, personally, I think there's good objections to heavy-OOP style programming and don't prefer it myself, but class is a perfectly convenient way to attach behavior and state together, which can be a useful thing.

olive topaz
#

Not sure if anyone has mentioned this yet, but you can use class syntax without writing ES5 style classes. Simply by setting your tsconfig target to ES5, and TS will compile your class syntax code to ES5 style classes code, without you having to change anything. See playground JS output.