1

I wanted to create a very simple function that takes any object and returns a new object with sorted properties, which is very simple in JavaScript:

function sortProperties(obj) {
  return Object.keys(obj).sort().reduce((accum, key) => {
    accum[key] = obj[key];

    return accum;
  }, {})
}

However, when adding types to it I'm having a lot of trouble to make the function accept any kind of object shape, throwing the following errors:

Type '{}' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to '{}'.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. No index signature with a parameter of type 'string' was found on type '{}'.

Link to Playground

Ideally this should be able to work with any incoming types as it's quite generic. I wanted to avoid using any because it otherwise complains about unsafe any elsewhere...

Any ideas how to make it stop complaining without @ts-ignore?

RecuencoJones
  • 2,747
  • 3
  • 22
  • 21
  • Sorting object properties is ... an odd thing to do (even though it's *possible* now, for certain types of property names). Why are you sorting properties? – T.J. Crowder Feb 18 '21 at 15:40
  • 1
    Also beware that that function is *lossy*. It loses the object's prototype, any non-enumerable properties, any inherited properties, and any Symbol-named properties. – T.J. Crowder Feb 18 '21 at 15:41
  • 1
    This would be applied to objects that will be eventually serialized to JSON, it's a very simple util function that I was using to compare some given JSON's which are already sorted. Yes, I could probably use other ways, but now that I got these errors I'd like to understand how it could be achieved with TypeScript! – RecuencoJones Feb 18 '21 at 15:44
  • 1
    That's the **one** use case I've had for doing it, too. :-D – T.J. Crowder Feb 18 '21 at 15:46

1 Answers1

2

There are a few steps in your code that the compiler cannot verify as type safe:

Object.keys(obj) returns string[], not Array<keyof T>, and for good reason. If you want the compiler to treat it as Array<keyof T>, you need to use a type assertion to say "hey, compiler, I know what I'm doing, and I vouch for the fact that Object.keys(obj) will be Array<keyof T>. If I'm wrong about that I will accept any runtime errors as my punishment, I won't blame you.".

Similarly, in reduce(), the compiler has no reason to believe that the initial object {} is a value of type T. And in fact it isn't, at least until you're all done. This is another opportunity for a type assertion to say "I swear that by the time anyone looks at this object it will be of type T, and if I'm wrong I will not blame you for not warning me."

Making those changes (plus a little generic callback for good measure) gives this:

function sortProperties<T>(obj: T): T {
    return (Object.keys(obj) as Array<keyof T>).sort().reduce(
        <K extends keyof T>(accum: T, key: K) => {
            accum[key] = obj[key];
            return accum;
        }, {} as T)
}

That generic callback with K extends keyof T is apparently not strictly necessary according to the compiler, but it's a good idea to do it so that it will complain if you somehow assigned the wrong property to the wrong key.

Anyway that compiles as desired. Keep in mind that if your runtime target happens to be ES5 (I assume it isn't) then property order is not guaranteed to match insertion order. Even with ES2015+ any numeric-looking keys will appear at the front no matter how you sort them. And a hundred other caveats, but you're likely aware of them!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • You're a saviour! Thanks for explaining it, it's pretty clear to me now! Super neat idea the generic callback as it gets rid of "any" possibilities :D – RecuencoJones Feb 18 '21 at 15:49