#Typing args for constructor()
41 messages · Page 1 of 1 (latest)
type someType = new (the: Args, go: Here) => TheType;
type example = new (a: string, b: 4) => Object;
Constructor of a class can't be generic nor can have return type annotation
How can this be used within a class?
Let's say I have a User class and I need some typing for the args of the new (or constructor) function
If you mean arguments, those go in the parenthesis:
class User {
constructor(arg1: string, arg2: number) {}
}
When you do (): Type that's a return type annotation, not an argument. And return type annotations aren't valid for constructors.
Yeah but I was wondering if I could 'group' those args using a single type, rather than having to type every single argument one by one
To avoid duplicate code
Do you mean an object?
If that's best practice, sure
It's whatever you want
type UserArg = {
name: string;
age: number
}
class User {
name;
age;
constructor(userData: UserArg) {
this.name = userData.name;
this.age = userData.age;
}
}
Oh that works? Lol, I remember I got some issues with it
Preview:```ts
type abc = (Arg1: 1, Arg2: 2) => void
declare class A {
constructor(...args: Parameters<abc>)
}```
It's completely vaild
Showing specific code you're having issues with can be helpful.
Is Parameters an already existing type?
Yes
Currently on my phone, will try in a bit
FWIW, I don't see the point of @edgy galleon's approach.
How come?
You can't use it in a class
What benefit does it have putting the arguments into another functions type then extracting it into the constructor, rather than just defining it in the constructor directly?
tbh I agree
it can be useful, sometimes, but I'd say it's better to list the arguments explicitly, so that if people are looking directly at the code they can see the full function signature
Is it the same concept as you provided earlier? But then you'd use args.name rather than userData.name
btw, the first thing I provided was me misunderstanding what you were asking
Oh my bad
But yeah, it's up to you if you want a single object argument for the constructor or individual arguments. Either approach works.
An advantage of single arguments is that TS has a shortcut for assigning them to properties:
class User {
constructor(public name: string, public age: number) {}
}
is short-hand for:
class User {
public name;
public age;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
The disadvantage is if your data is already object-based, you may have to do something like new User(data.name, data.age) instead of new User(data).
Then why even have a User class?
Alright, imagine this
All my classes represent models from my backend
I have a base class for all my classes, containing the following
export default class Base {
id?: string;
createdAt?: Date;
updatedAt?: Date;
}
Now imagine a product class. When I fetch data from the backend, I'd like to create a new Product instance where it passes the response data.id, data.created_at and data.updated_at to the super() of the Product's constructor.
When i want to create a new Product and send it to the backend, I would have an existing Product instance and serialize this with a serialize method so i can pass the data as json
But, this existing product cannot have the id, created an updated from the Base class and the args for the Base class shouldnt even be passed when creating this existing Product instance in the first place
My ideal approach would be this for all my models:
// creating a new Product instance from an API response
const product = new Product(data.id, data.created_at, data.updated_at, data.name)
// creating a new Product instance that will be sent to the backend
const product = new Product(someProductName)
api.post('some-url', product.serialize())
Is it just best practice for me to always pass an object to the constructor, whereas id, created_at and updated_at are undefined by default?
something like this:
Base
import { Base as IBase } from './types';
export default class Base implements IBase {
id?: string;
createdAt?: Date;
updatedAt?: Date;
}
Product
import Base from "@models/Base";
import { Product as IProduct } from './types';
type ConstructorArgs = Base & IProduct
export default class Product extends Base {
constructor(args: ConstructorArgs) {
super()
}
}
Because then, args has both my Base props and Product props
not sure if thats a best practice approach to have both a Product interface and a Product class
@sly monolith My personal suggestion is to not wrap data into classes if you don't have a strong reason to. Personally, I would use the Product type itself and not create these class wrappers.
What would be a good use-case for a class rather than a type?
I like classes for non-data usages, e.g. a database adaptor, logger, etc.
But I think serializing data to classes, mutating those classes, and deserializing adds a lot of boilerplate and fairly little value.
And if you've got some specific framework or pattern that needs class based models (e.g. some ORM) then it'd make sense. (But then usually the framework itself will generate a lot of the 'boilerplate' for you)
On that I must agree. Serializing might not have been the best example given. I expect the classes to have some utils as methods, or some logic such as a first_name and last_name that will be combined with a name getter for example