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 writing a new API from scratch at work, I decided to use the opportunity to learn TypeScript by jumping into the deep end.
So far, here are my positive takeaways:
- I'm a huge fan of the added intellisense in my IDE (VS Code). I've always found the intellisense for regular JavaScript packages to be a bit flaky for some reason, but it's rock solid with TypeScript.
- The "might be undefined" checks have definitely saved me some time by pointing out places where I need to add a few null checks after
.get()
ing something from aMap
, etc. - I have always liked being able to spell out my classes in JavaScript; I've often gone to extreme lengths to document JS classes with JSDoc.
But I've run into a few significant frustrations that have really slowed me down, and I'm hoping some of my much more experienced TypeScript DEV friends will be able to help me figure them out! 😎
Class types
I can't figure out how to use or declare class types, especially when I need to pass around subclasses that extend a certain base class. This came up for me because I'm using Objection.js, an ORM package that makes heavy use of static getters on classes. I need to pass around subclasses of Objection's Model
class to check relationship mappings and make queries, so I need a way to say, "This parameter is a class object that extends Model
". I wish I had something like:
function handleRelations(modelClass: extends Model) ...
The best I've found so far is to use a rather annoying interface and update it every time I need to use another method from Objection's extensive API, like:
interface IModel {
new(): Model
query(): QueryBuilder
tableName: string
idColumn: string | string[]
relationMappings: object
// etc.
}
function handleRelations(modelClass: IModel) ...
This works, but it's rather annoying to have to reinvent the wheel this way. Is there a more explicit way to tell TypeScript, "I mean a class extending this type, not an instance of this type"?
Overriding methods with different return types
This is more a best-practice question than anything else. I've run into some cases where a base class declares a method that returns a particular type, but subclasses need to override that method and return a different type. One example is the idColumn
static getter used by Objection models, which can return either a string
or a string[]
.
I've found that if I simply declare the base class as returning one type and the subclass as returning another, I get yelled at:
class Animal extends Model {
static get idColumn(): string {
return 'name'
}
}
class Dog extends Animal {
static get idColumn(): string[] {
return ['name', 'tag']
}
}
/* ERROR
Class static side 'typeof Dog' incorrectly extends base class static side 'typeof Animal'.
Types of property 'idColumn' are incompatible.
Type 'string[]' is not assignable to type 'string'.
*/
If I declare the base class with a Union type, that seems to work, although adding another layer of subclass trying to use the original base class's type no breaks because of the middle class:
class Animal extends Model {
static get idColumn(): string | string[] {
return 'name'
}
}
class Dog extends Animal {
static get idColumn(): string[] {
return ['name', 'tag']
}
}
class Poodle extends Dog {
static get idColumn(): string {
return 'nom'
}
}
/*
Class static side 'typeof Poodle' incorrectly extends base class static side 'typeof Dog'...
*/
So I'm now torn. I like to be as specific as I can in my method signatures, but it seems I have two choices here: either always use the full union type string | string[]
as the return type of the idColumn
getter for all subclasses, or simply don't declare a return type for subclasses, only the base class:
class Animal extends Model {
static get idColumn(): string | string[] {
return 'name'
}
}
class Dog extends Animal {
// this?
static get idColumn(): string | string[] {
return ['name', 'tag']
}
// or this?
static get idColumn() {
return ['name', 'tag']
}
}
So my question here is, which is better? Is there an accepted paradigmatic solution here? I don't really like either; the former feels slightly misleading, but the latter feels incomplete. I'm leaning toward the latter in this case since it's immediately obvious what the type of a constant return value is, but in more complex cases involving an actual method with some complicated logic, I'm not sure how I'd handle it.
Dealing with simple objects
Okay, this is a more minor annoyance, but it really bugs me. If I just want to say, "This function accepts/returns a plain ol' object with arbitrary keys and values", the only syntax I can find is:
{ [key: string] : any }
Used once on it's own, that's not the worst thing I've ever seen, but I have a method that accepts an object-to-object Map and returns another one, and the method signature looks like this:
function converter(input: Map<{ [key: string] : any }, { [key: string] : any }>): Map<{ [key: string] : any }, { [key: string] : any }>
That's... that's not okay. I've run into more complex example as well, cases where I'm declaring interfaces with nested objects and such, and this syntax makes them near impossible to read. So my solution has been to declare a trivial interface called SimpleObject
to represent, well, a simple object:
interface SimpleObject {
[key: string] : any
}
And like, that works, but whenever I show anyone my code I have to explain this situation, and it just seems like an oversight that there's apparently no native name for simple objects in TypeScript. Am I missing something?
Conclusion
Thanks to anyone who took the time to read this, and thanks a million to anyone who helps me out or leaves a comment! I'm enjoying TypeScript on the whole, and I'm sure little quirks like this will become natural after a while, but if there is a better way to handle them, I'd love to know! 😁
Top comments (70)
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.
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
andrelationshipMappings
to represent database constraints. Others may feel differently, but it's how my brain works.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 typestring | 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.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 likethis code is going to fail if it unexpectedly receives
string | string[]
because it is expecting to have astring
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.
@trusktr Which thing are you referencing? This is a pretty long thread at this point lol
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...
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.
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.
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?
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.
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.
Saying the class is more complex is subjective.
It is not subjective unless you can point out what can not be achieved without class.
Hi Edward, I have the same experience with your approach to use
type
andfunctions
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.
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?
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.
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.
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 experienceclass
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?
vs...
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:
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.
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?
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:
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:
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!
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:
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.
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.
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.
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 theclass
keyword. And at that point, I honestly feel thatclass
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:
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
class
es 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.This convo is fun :D
I'll throw some opinion that hopefully will help you with this.
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.
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.aReflection
. 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:
And operations should be written like how we describe a scene in a story, step by step, chronologically.
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).
The code above shows a really easy way to get a pretty complex idea done in a simple manner, only by separating
operations
andstructure
as opposed to the classical OOP wheremethod
andproperty
is in one place so that it's confusing whether we should writeattack()
orreceiveDamage()
.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 returnunknown
. You could practically giveunknown
or if not possibleany
(forgive me, lord) to theidColumn
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.
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.
And using the fetch function would be
If you're really sure that your database will always return the correct schema, do this, but I don't recommend this.
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.
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.
@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 😁
@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.
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 :)
So we pretty much have the same background :)
I think my biggest complaint about JS classes is the encapsulation. Let's see the following code as example:
As you can see
person.talk
is not encapsulated as a part of the classPerson
. 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 tobind
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.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
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.
Actually, class object's context is mutable. Let's take a closer look at the example again:
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.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:
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:Regardless of how we built the
person
object, what's confusing is that thetalk
method loses itsthis
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:
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 ofmyBtn.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 theclass
syntax. Unpopular opinion, but IMO it will be even better once the class field and private class field syntaxes become standard.I think that is a fair statement. Regardless, that was fun discussion and I think I learnt something from it :)
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
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?
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:
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:
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.
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.
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 todata-service.js
, easily see the available methods, and ctrl+click stright togetVendor()
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.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:
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:And
const
andlet
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.
Ken thank you for spending so much time on this. Definitely something for me to study.
Thanks for the amazing quote! I copied to my twitter. Hope it's OK.
twitter.com/kore_sar/status/121395...
No problem!
For the first one
function handleRelations(modelClass: extends Model) ...
here is a very simple TypeScript Playground example example.
TLDR;
In terms of readability for
see this TypeScript Playground example
TLDR;
When I have a chance, I'll check out the other issues you're having.
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 extendingModel
, 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!
Ahh, I see what you mean. My bad. You can do this
TLDR;
Does that handle your use case?
Oh man yes it does, that's exactly what I was looking for! I didn't realize that was a thing! Thanks!
Or even better using generics:
Generics for sure, but it still needs to be
typeof Model
as he doesn’t want an instance of the class.I missed that. In that case, he shouldn't use a
class
here, rather: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 aninterface
ortype
is used and he wanted to create an instance of theModel
class, using aninterface
ortype
would not work. If he wanted to use it for duck typing in his function, then aninterface
ortype
would suffice.Here's a sample TypeScript Playground for those interested.
Very interesting. I didn't realise you could combine
new
withtypeof T
params.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 ofModel
, so I need to pass around the classes themselves.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)
is a bit more useful. Stick in /shared/ or wherever you keep such things.
You beat me to it!
typeof Model
andT 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.is a
is strong, but can be loosened with Factories and Bridge PatternHaven't found a good use for the Bridge Pattern, but I've often wondered why more ORMs aren't using Factories/mixins
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 ofstatic 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.
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.
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.
It can help to be able to ask quick questions on gitter.im/Microsoft/TypeScript or launchpass.com/fullstacktypescript
Awesome, thanks! I'll keep those groups handy
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...
The second point, absolutely. Not the first point though; what I was reaching for but couldn't find was
typeof Model
, as infunction foo(model: typeof Model)
.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.
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.
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.
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.
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.