Home > other >  How can I implement custom class with overridable list type?
How can I implement custom class with overridable list type?

Time:11-03

I have a Nextjs project using next v10.1.2 and Typescript v4.4.4 I would like to have a class SortableList that encapsulates a list of a custom type. The only requirement is that the list extends my abstract class OrderedItem.

Currently, I am getting an error when I try to access a property on a class that extends OrderedItem from a new SortableList object.

Is this possible in Typescript and what is the correct way to set this up?

Code:

OrderedItem.ts

// The purpose of this class is to ensure that items in a list can be ordered.
abstract class OrderedItem {
  order: number

  constructor(order: number) {
    this.order = order
  }
}

export default OrderedItem

CustomListItem.ts

import OrderedItem from './OrderedItem'

// This is an example of a class that I would like to be orderable.
class CustomListItem extends OrderedItem {
  name: string
  jsx: JSX.Element

  constructor(order: number, name: string, jsx: JSX.Element) {
    super(order)
    this.name = name
    this.jsx = jsx
  }
}

export default CustomListItem

SortableList.ts

import OrderedItem from './OrderedItem'

class SortableList {
  // I want this array to be overridden by a custom class like CustomListItem
  list: OrderedItem[]

  constructor(list: OrderedItem[]) {
    this.list = list
  }

  sortByOrder = (): void => {
    this.list = this.list.sort((a, b) => a.order - b.order)
  }

  sortByReverseOrder = (): void => {
    this.list = this.list.sort((a, b) => b.order - a.order)
  }
}

export default SortableList

example.tsx

import CustomListItem from './CustomListItem'
import SortableList from './SortableList'

export default function Example() {
  const customList: CustomListItem[] = [
    new CustomListItem(3, 'name3', <div></div>),
    new CustomListItem(1, 'name1', <div></div>),
    new CustomListItem(2, 'name2', <div></div>),
  ]
  const sortableList: SortableList = new SortableList(customList)
  sortableList.sortByOrder()

  return sortableList.list.map(item => item.jsx)
}

The error I am getting from this example is in the return statement of example.tsx when trying to access the jsx property on item since item is typed as OrderedItem and not CustomListItem.

Error: Property 'jsx' does not exist on type 'OrderedItem'.

What is the correct way to do this? I tried messing around with declaring SortableList with <T> and initializing it like

const sortableList = new SortableList<CustomListItem>(customList), but I'm not familiar with how that is supposed to work.

CodePudding user response:

The problem is when you access an element of your SortableList the compiler knows just that it is an OrderedItem which has no property jsx.

There are two main solutions to this:

  1. You know that your item will be always of a certain type (like in your case) and you cast it to that type. Easy to implement, but I really warn you that this is not a good idea, because you are going to break object programming principles and also this can lead to runtime errors which are very hard to debug, for example you put an item which is not of CustomListItem in the list and has no property jsx. This is how it is done:
sortableList.list.map(item => (item as CustomListItem).jsx)
  1. The formal way is to use generics. A generics is just a placeholder for a type in your code, when you use it, it will be replaced by the specified type. In your case you need furthermore a constraint on that generic which force the implementation specified type to extends your OrderedItem class. In this way there are no pratical cases where you will get an unexpected error, because the ts compiler prevent them by checking your types. This is how it is done in your case:

SortableList.ts

import OrderedItem from './OrderedItem'

// T is the generic
// T extends OrderedItem is the constraint
class SortableList<T extends OrderedItem> {
  // this array can contains only elements
  // that are descendant of T
  // T is always a descendant of OrderedItem
  list: T[]

  constructor(list: T[]) {
    this.list = list
  }

  sortByOrder = (): void => {
    this.list = this.list.sort((a, b) => a.order - b.order)
  }

  sortByReverseOrder = (): void => {
    this.list = this.list.sort((a, b) => b.order - a.order)
  }
}

export default SortableList

example.tsx

import CustomListItem from './CustomListItem'
import SortableList from './SortableList'

export default function Example() {
  const customList: CustomListItem[] = [
    new CustomListItem(3, 'name3', <div></div>),
    new CustomListItem(1, 'name1', <div></div>),
    new CustomListItem(2, 'name2', <div></div>),
  ]
  const sortableList: SortableList<CustomListItem> =
         new SortableList<CustomListItem>(customList)
  sortableList.sortByOrder()

  return sortableList.list.map(item => item.jsx)
}
  • Related