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
JWP • 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
 
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.

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
 
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
 
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
 
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
 
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
 
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
JWP • 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
JWP • 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
JWP • 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
 
kristijanfistrek profile image
KristijanFištrek

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

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
 
jwp profile image
JWP • 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