You’ve written this function before.

type Item = {
  foo: number,
}

const sortByFooAsc = (items: Item[]) => {
  return [...items].sort((a, b): number => {
    return a.foo - b.foo
  })
}

describe('sortByFooAsc', () => {
  it('should order items by `foo` ascending', () => {
    const items = [
      { foo: 99 },
      { foo: 33 },
      { foo: 66 },
    ]
    const actual = sortByFooAsc(items)
    const expected = [
      { foo: 33 },
      { foo: 66 },
      { foo: 99 },
    ]
    expect(actual).toEqual(expected)
  })
})

How do you make it reusable, generic?

First, make the field variable.

type Item = {
  foo: number,
}

const sortByAsc = (field: string, items: Item[]) => {
  return [...items].sort((a, b): number => {
    return a[field]- b[field]
  })
}

describe('sortByAsc', () => {
  it('should order items by `foo` ascending', () => {
    const items = [
      { foo: 99 },
      { foo: 33 },
      { foo: 66 },
    ]
    const actual = sortByAsc('foo', items)
    const expected = [
      { foo: 33 },
      { foo: 66 },
      { foo: 99 },
    ]
    expect(actual).toEqual(expected)
  })
})

This works, but TypeScript is unhappy.

Fix that by specifying that field has to be one of the keys of Item.

- const sortByAsc = (field: string, items: Item[]) => {
+ const sortByAsc = (field: keyof Item, items: Item[]) => {

Let’s try it with another type.

type Item = {
  foo: number,
}

type Thing = {
  bar: number,
}

const sortByAsc = (field: keyof Item, items: Item[]) => {
  return [...items].sort((a, b): number => {
    return a[field]- b[field]
  })
}

describe('sortByAsc', () => {
  it('should order items by `foo` ascending', () => {
    const items = [
      { foo: 99 },
      { foo: 33 },
      { foo: 66 },
    ]
    const actual = sortByAsc('foo', items)
    const expected = [
      { foo: 33 },
      { foo: 66 },
      { foo: 99 },
    ]
    expect(actual).toEqual(expected)
  })

  it('should order things by `bar` ascending', () => {
    const things: Thing[] = [
      { bar: 99 },
      { bar: 33 },
      { bar: 66 },
    ]
    const actual = sortByAsc('bar', things)
    const expected = [
      { bar: 33 },
      { bar: 66 },
      { bar: 99 },
    ]
    expect(actual).toEqual(expected)
  })
})

This works, but TS is unhappy. sortByAsc only works for Item. Let’s use a generic.

const sortByAsc = <T>(field: keyof T, items: T[]) => {
  return [...items].sort((a, b): number => {
    return a[field]- b[field]
  })
}

Better, but it doesn’t know what these field values may be. It’s too generic. To fix that, specify what these fields are.

const sortByAsc = <T extends {[key: string]: number}>(field: keyof T, items: T[]) => {

T will have string keys and the values will be numbers.

Now we are constrained by the actual keys of the type we’re sending in.

const things: Thing[] = [
  { bar: 99 },
  { bar: 33 },
  { bar: 66 },
]
const actual = sortByAsc('banana', things)

But what if the type has various key/value pairs, not all of which are numbers?

it('should order things by `bar` ascending', () => {
type Thing = {
  bar: number,
  colors?: string[], // now we have optional `colors`
}
const things: Thing[] = [
  { bar: 99 },
  { bar: 33 },
  { bar: 66 },
]
const actual = sortByAsc('bar', things)
const expected = [
  { bar: 33 },
  { bar: 66 },
  { bar: 99 },
]
expect(actual).toEqual(expected)

We can specify a new type with just the number fields.

type ThingNumbers = Pick<Thing, 'bar'>

const things: Thing[] = [
  { bar: 99 },
  { bar: 33 },
  { bar: 66 },
]
const actual = sortByAsc<ThingNumbers>('bar', things)

But how can we dynamically create this so additions/changes to Thing don’t need to be manually updated?

type Thing = {
  bar: number,
  colors?: string[],
}

type PickByValue<T, ValueType> = Pick<
  T,
  {[key in keyof T]-?: T[key] extends ValueType ? key : never}[keyof T]
>

type ThingNumbers = PickByValue<Thing, number>

This is some near-magic that allows you to pick a subset of fields from a type where the field values match some type.

Now we can alter the type without bothering to modify this selection of fields manually.


type Thing = {
  bar: number,
  apple: number,
  colors?: string[],
}

type PickByValue<T, ValueType> = Pick<
  T,
  {[key in keyof T]-?: T[key] extends ValueType ? key : never}[keyof T]
>

type ThingNumbers = PickByValue<Thing, number>

const things: Thing[] = [
  { bar: 99, apple: 1 },
  { bar: 33, apple: 1 },
  { bar: 66, apple: 1 },
]
sortByAsc<ThingNumbers>('apple', things)
sortByAsc<ThingNumbers>('colors', things)
const actual = sortByAsc<ThingNumbers>('bar', things)

The final step would be to have the sort function automatically handle this for us so the callsites don’t need to define these “number field” types.

type PickByValue<T, ValueType> = Pick<
  T,
  {[key in keyof T]-?: T[key] extends ValueType ? key : never}[keyof T]
>

const sortByAsc = <T extends PickByValue<T, number>>(field: keyof PickByValue<T, number>, items: T[]) => {
  return [...items].sort((a, b): number => {
    return a[field]- b[field]
  })
}

This is practically unreadable, but it works. You can see that the fields are discriminated using the number type.