Home > OS >  Way to define properties on typescript class without defining type
Way to define properties on typescript class without defining type

Time:08-24

I have a class below Time. I'm curious if there's a way to define the 3 properties different syntactically preferably in a way where I didn't have to define the properties above.

export class Time {
  parsed: ParsedTime
  string: string
  minutes: number
  constructor (public time: TimeConstructable) {
    this.parsed = parseResolve(this.time); 
    this.string = toString(this.parsed)
    this.minutes = inMinutes(this.parsed);
  }
}

I've tried this but it doesn't work because these run before initialization and don't have access to this.time

export class Time {
  constructor (public time: TimeConstructable) {}

  parsed = parseResolve(this.time); 
  string = toString(this.parsed)
  minutes = inMinutes(this.parsed);
}

Getters are nice and have a clean syntax but the values are calculated everytime the property is retrieved, and not cached as in the first example:

export class Time {
  constructor (public time: TimeConstructable) {}

  get parsed () {
    return parseResolve(this.time); 
  }
  
  get string () {
    return toString(this.parsed)
  }

  get minutes () {
    return inMinutes(this.parsed);
  }
}

This is a bit unorthodox but is an interesting syntax, downside is it allows for a complicated and unintentional constructor typing.

export class Time {
  constructor (
    public time: TimeConstructable,
    public parsed = parseResolve(time),
    public string = toString(parsed),
    public minutes = inMinutes(parsed)
  ) {}
}

Any others?

CodePudding user response:

The class isn't doing anything useful for you here because it doesn't have any methods. Classes are nice to tie together instance data with methods that operate on that data - but if one of those is missing, it makes more sense to have a plain function or plain object instead.

This may not be suitable for your actual use case, but given the code in the question, you might use something like:

export const makeTime = (time: TimeConstructable) => ({
  parsed: parseResolve(time),
  string: toString(parsed),
  minutes: inMinutes(parsed),
});

And then there's barely any need for type annotations at all.

(Personally, one of the reasons I prefer to avoid classes is because functions are nicer to type - TypeScript's automatic inference can do almost all of the type-work for you, letting you focus solely on the program's logic)

CodePudding user response:

Unfortunately, the class syntax requires explicit members declaration.

That being said, as you figured out, there is a handy TypeScript shorthand that lets you declare them directly as constructor arguments, since it is so common to assign them that way. With this approach, we can write them only once.

But that shorthand works only for direct assignment. You need uncommon logic if you try combining it with conversion, leading to loopholes as you pointed out.

The common solution is indeed to write it as in your 1st sample: explicit members declaration, a conversion method, and assigning the converted values within the constructor body. That gives you at least twice writing the members names, or even more if you have a separate conversion method (useful if you need to update the value later on).

Another solution indeed leverages getters.

Getters [...] values are calculated everytime the property is retrieved, and not cached as in the first example

A classic workaround implements lazy computation (as you have done) combined with private cache. E.g.:

class Time {
  private _parsed: ParsedTime;

  get parsed() {
    // if not cached yet, lazy evaluate
    // (I assume null and undefined are not expected results)
    this._parsed ??= parseResolve(this.time);
    return this._parsed;
  }
}

IIRC, there should be some libraries that offer decorators for this use case, since it is also quite common. But it is not very complex to write it oneself. With such decorator, you may need even less writing.

  • Related