#Typing args for constructor()

41 messages · Page 1 of 1 (latest)

sly monolith
#

Is there any way to easily type the arguments of a constructor? As constructor<Type>() {} or constructor(): Type {} is not allowed.

Am I limited to typing each argument individually?

edgy galleon
terse spire
sly monolith
gleaming summit
#

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.

sly monolith
#

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

gleaming summit
#

Do you mean an object?

sly monolith
#

If that's best practice, sure

terse spire
#

It's whatever you want

gleaming summit
#
type UserArg = {
    name: string;
    age: number
}
class User {
    name;
    age;
    constructor(userData: UserArg) {
      this.name = userData.name;
      this.age = userData.age;
    }
}
sly monolith
#

Oh that works? Lol, I remember I got some issues with it

edgy galleon
#

that could work

#

or

thorn obsidianBOT
#
Alexthealex#9367

Preview:```ts
type abc = (Arg1: 1, Arg2: 2) => void

declare class A {
constructor(...args: Parameters<abc>)
}```

terse spire
gleaming summit
#

Showing specific code you're having issues with can be helpful.

sly monolith
#

Is Parameters an already existing type?

terse spire
#

Yes

sly monolith
#

Currently on my phone, will try in a bit

gleaming summit
#

FWIW, I don't see the point of @edgy galleon's approach.

sly monolith
#

How come?

terse spire
#

You can't use it in a class

gleaming summit
#

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?

edgy galleon
sly monolith
edgy galleon
#

btw, the first thing I provided was me misunderstanding what you were asking

sly monolith
#

Oh my bad

gleaming summit
#

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).

terse spire
#

Then why even have a User class?

sly monolith
#

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

gleaming summit
#

@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.

sly monolith
#

What would be a good use-case for a class rather than a type?

gleaming summit
#

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)

sly monolith
#

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