Sort it Out
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 number
s.
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
number
s?
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.