Typescript model return type of Array.prototype.sort()

Viewed 118

I have defined a type for "sorted" arrays as follows:

type Sorted<T> = T[];

So right now it just gives the developer some "awareness" that they need to pass a sorted array of some type and they need to ensure the sorting by themself.

I know that I can't check with Typescript whether an array is sorted. But can I somehow change the return type of Array.prototype.sort() to be a sorted array of whatever is put in?

2 Answers

You can use declaration merging to prepend an overload call signature to Array.prototype.sort, like this:

declare global {
    interface Array<T> {
        sort(compareFn?: ((a: T, b: T) => number) | undefined): Sorted<T>;
    }
}

Here I am using global augmentation (wrapped with declare global {...}) which assumes that your code is in a module. If your code is not in a module, you can leave out the declare global wrapper.

Once you do this, you can see from IntelliSense that calls to sort() result in a Sorted:

const arr = [8, 6, 7, 5, 3, 0, 9];
const sortedArr = arr.sort();
// const sortedArr: Sorted<number>;

As you wanted. This is the end of the answer to the question as asked.


Unfortunately, since you've defined Sorted<T> to be an alias for T[], and because TypeScript's type system is structural and not nominal, the compiler really sees no meaningful difference between Sorted<T> and T[]. The developer may or may not use IntelliSense to see Sorted<T>, there will be no compile-time error if the developer fails to notice this and passes an unsorted array into a function that wants a sorted one:

function acceptSorted<T>(sortedArr: Sorted<T>) { }
acceptSorted(arr); // no error

If you'd like to prevent this, you can use a trick called "branding" where you add a phantom property to the type that distinguishes a T[] from a Sorted<T>:

type Sorted<T> = T[] & { __sorted: true };

By "phantom property" I mean it does not actually exist at runtime. But the compiler doesn't realize this:

function acceptSorted<T>(sortedArr: Sorted<T>) { }
acceptSorted(arr); // error!
// --------> ~~~
// Argument of type 'number[]' is not assignable to parameter of type 'Sorted<number>'.
acceptSorted(sortedArr); // okay

This still doesn't do much to enforce that a Sorted<T> is actually sorted, since you can modify the list to make it unsorted:

sortedArr.reverse()
console.log(sortedArr); // [9, 8, 7, 6, 5, 3, 0];
acceptSorted(sortedArr); // no error

But I'll stop here without trying to make a truly "safe" Sorted type, as it would bring us even further out of scope of the original question.

Playground link to code

What about a type and a function combination?

type SortedList<T> = T[];

function SortedList<T extends string | number>(arr: T[]): SortedList<T> {
    return arr.sort();
}

const names = SortedList<number>([]);

Related