0

I have an interface with an extensive list of properties, each with different types and structures:

interface OrderInterface {
  id: number;
  orderNumber: string;
  createdAt: string;
  updatedAt: string;
  customer: {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    address: {
      street: string;
      city: string;
      state: string;
      zip: string;
      country: string;
    }
  };
  products: {
    id: number;
    name: string;
    qty: number;
    isVirtual: boolean;
  }[];

  // ... assume we add an arbitrary number of fields.

}

Initially, the interface satisfied my needs for simply passing data around and ensuring the structure is the same in all parts used and to satisfy TypeScript's checking. Now, I find myself needing to perform some functions on the data which would be easy if methods were tied to the data directly. I start to write the class, and I realize that my first inclination is that I have to copy and assign every single property in the constructor.

class Order implements OrderInterface {

  id: number;
  orderNumber: string;
  createdAt: string;
  updatedAt: string;
  customer: {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    address: {
      street: string;
      city: string;
      state: string;
      zip: string;
      country: string;
    }
  };
  products: {
    id: number;
    name: string;
    qty: number;
    isVirtual: boolean;
  }[];

  // Assume the arbitrary fields defined in interface are defined here too

  constructor(data: OrderInterface) {
    this.id = data.id;
    this.orderNumber = data.orderNumber;
    this.createdAt = data.createdAt;
    // ...
    // And I have to duplicate every property? What if a property changes? Is there a better way?

  }

  get customerName(): string {
    return `${this.customer.firstName} ${this.customer.lastName}` || '(name not available';
  }

  get physicalProducts(): Product[] {
    // filter products by isVirtual property and return result.
  }

  // ... assume more functions to get, compare, change data, etc.

}

interface Product {
  id: number;
  name: string;
  qty: number;
  isVirtual: boolean;
}

In plain JS, I think this would work in the constructor. Essentially, it would just copy all the fields from my interface to the object.

constructor(data: OrderInterface) {
  Object.assign(this, data);
} 

But with TypeScript, I get an error that resembles this (changed from my real code)

Error: src/app/shared/inquiry.service.ts:378:3 - error TS2564: Property 'id' has no initializer and is not definitely assigned in the constructor.
Error: src/app/shared/inquiry.service.ts:379:3 - error TS2564: Property 'orderNumber' has no initializer and is not definitely assigned in the constructor.
Error: src/app/shared/inquiry.service.ts:380:3 - error TS2564: Property 'createdAt' has no initializer and is not definitely assigned in the constructor.
Error: src/app/shared/inquiry.service.ts:381:3 - error TS2564: Property 'updatedAt' has no initializer and is not definitely assigned in the constructor.
Error: src/app/shared/inquiry.service.ts:382:3 - error TS2564: Property 'customer' has no initializer and is not definitely assigned in the constructor.
Error: src/app/shared/inquiry.service.ts:395:3 - error TS2564: Property 'products' has no initializer and is not definitely assigned in the constructor.

// And continued Error for arbitrary property having no initializer nor being assigned in constructor

Even though technically all the correct fields should be converted to the new class instantiation, TypeScript doesn't seem to recognize this happening. What is an elegant way to do this in the constructor?

Or, is trying to convert this interface to a class "not the Angular way" to begin with? Is it more "Angular" to keep the data as an interface and put any functions dealing with model(s) in service classes?

  • You get that error because you haven't defined `Order`'s members, and so TS believes that it is incorrectly implementing `OrderInterface`. – kelsny Jan 24 '23 at 19:14
  • In this case you could probably just ditch the interface and make it just the class. – Gunnar B. Jan 24 '23 at 20:54
  • I would recommend to do it the "Angular way" as you mentioned in the last part of your question. Otherwise it will be really painful to implement and especially to maintain. – KSoze Jan 24 '23 at 22:34
  • @vera. great catch! I've updated my question with the fields defined and the new errors that happen – Chuck Dietz Jan 26 '23 at 19:43
  • Does this answer your question? [Property '...' has no initializer and is not definitely assigned in the constructor](https://stackoverflow.com/questions/49699067/property-has-no-initializer-and-is-not-definitely-assigned-in-the-construc) – Yury Tarabanko Jan 26 '23 at 19:57
  • Basically you have 3 options: explicitly assign, disable `strictPropertyInitialization` check, or mark your properties with exclamation mark to disable it only for certain places in your code `id!: number` – Yury Tarabanko Jan 26 '23 at 20:02
  • @YuryTarabanko I didn't see your comments come in until after I put in an answer, but this was a big part of what I was looking for, thanks for this! I plan to go with option 1 or 3 if I come across this again, as I like the strictPropertyInitialization for most of the code except just this case with mass-assignment. – Chuck Dietz Jan 26 '23 at 20:38

1 Answers1

0

Per @KSoze's recommendation in their comment, I decided to keep the "data" as an interface and the "business logic" as a service:

order.model.ts

export interface OrderInterface {
  id: number;
  orderNumber: string;
  createdAt: string;
  updatedAt: string;
  customer: {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
  address: {
    street: string;
    city: string;
    state: string;
    zip: string;
    country: string;
  };
  products: {
    id: number;
    name: string;
    qty: number;
    isVirtual: boolean;
  }[];
}

order.service.ts

import {OrderInterface} from './order.model.ts';

export class OrderService {
  get customerName(order: OrderInterface): string {
    return `${order.customer.firstName} ${order.customer.lastName}` || '(name not available';
  }

  get physicalProducts(order: OrderInterface): Product[] {
    // filter products by isVirtual property and return result.
  }
}

If I were to do this again more-similarly to my original intent, I could use the "not-null assertion operator" on every required field (assuming I didn't have a default value I could set to the field instead). From my experience, the class fields would still be type-checked in their usage other than the constructor.

export class Order implements OrderInterface {
  id!: number;
  orderNumber!: string;
  createdAt!: string;
  updatedAt!: string;
  customer!: {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    address: {
      street: string;
      city: string;
      state: string;
      zip: string;
      country: string;
    }
  };
  products!: {
    id: number;
    name: string;
    qty: number;
    isVirtual: boolean;
  }[];

  constructor(data: OrderInterface) {
    Object.assign(this, data);
    // No errors. Typescript's "!" operator assumes the fields are defined upon instantiation.
  }
}