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:
- 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 propertyjsx
. This is how it is done:
sortableList.list.map(item => (item as CustomListItem).jsx)
- 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)
}