Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I write a generic TypeScript function that takes a mutable or immutable array as parameter

I tried to write the following method.

It started getting very complicated, because now every method that receives the result from catchUndefinedList must be capable of handling mutable and immutable arrays.

Can anybody help me please?

/**
 * Catch any errors with a list.
 */
export function catchUndefinedList<T> (list: readonly T[] | T[]): readonly T[] | T[] {
  return nullOrUndefined(list) || !list?.length ? [] : list
}

EDIT: added nullOrUndefined

export function nullOrUndefined (el: any): el is (undefined | null) {
  return typeof el === 'undefined' || el === null
}

EDIT: added some easy example to showcase the problem.

As can be seen catchUndefinedList receives an list1, which is a mutable array, as parameter. In this case it will then output an immutable array even though list1 was mutable and the return of catchUndefinedList is simply the parameter list when list is not undefined.

When trying to push to list2 it will fail due to immutability and return TS2339.

const list1 = ['foo']
const list2 = catchUndefinedList(list1)
list2.push('bar')
like image 581
jpkmiller Avatar asked Apr 29 '26 12:04

jpkmiller


1 Answers

First of all, this question only really makes sense if null or undefined are allowed in the type for list. So I am going to assume you meant to allow that.


It sounds like you want the return value of the function to be the same type of array as the input. That means your function needs be generic on the array, not just the member type of that array. This is because the readonly-ness of the array is the part of the array type, and not the members.

That might look something like this:

export function catchUndefinedList<
  T extends readonly unknown[]
> (list: T | null | undefined): T {
  return (
    nullOrUndefined(list) ||
    !list?.length
      ? [] // Type 'T | never[]' is not assignable to type 'T'.
      : list
  )
}

// mutable
const list1 = ['foo']
const list2 = catchUndefinedList(list1)
list2.push('bar')

// immutable
const immlist1: readonly string[] = ['foo']
const immlist2 = catchUndefinedList(immlist1)
immlist2.push('bar') // Property 'push' does not exist on type 'readonly string[]'.(2339)

Here you can see that the push on the mutable array is allowed, but the push on the immutable array is not. This is good.


However, this does present a problem with tuples. This is reason for that type error in my code snippet above. Imagine you called this function like so:

const tuple2 = catchUndefinedList<[string, number, boolean]>(undefined)
tuple2[0].split('') // no type error, instead there is a runtime error

This is a problem because your function returns [] in the undefined case, which is not a valid type for this tuple.

You can silence the error with a [] as unknown as T but that's not really recommended. If someone does try to do this with a tuple, you will probably get runtime errors in strange places.

I'm not sure how to constrain this properly, to be honest, since all tuples are subtypes of unbounded arrays.

Payground


That said, this seems pretty complex for a simple null check. Are you sure this is the path you want to go down?

All this code does is:

  • when list is null or undefined it returns a new array of zero items
  • when list is an array of zero items, it returns a new array of zero items
  • when list is an array of one or more items, it returns list.

Which seems to me would be practically identical to the Nullish coalescing operator ??

list ?? []

The only difference between your code and this line is that when there is zero items list is returned instead of a new empty array. But it's still an empty array, so that distinction probably doesn't matter.

But this behaves sensibly in all the above cases:

// mutable
const list1 = ['foo'] as string[] | undefined
const list2 = list1 ?? []
list2.push('bar')

// immutable
const immlist1 = ['foo'] as readonly string[] | undefined
const immlist2 = immlist1 ?? []
immlist2.push('bar') // type error

// tuple
const tuple1 = ['a', 1] as [string, number] | undefined
const tuple2 = tuple1 ?? []
tuple2.push('c') // type error

Playground

like image 179
Alex Wayne Avatar answered May 01 '26 10:05

Alex Wayne



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!