This article discusses the implementation of an implicit pick, the reason for it & what makes it special.
Here is a Typescript Playground with examples that will be used as a reference. Can also be found via my Github Repo. {% github Prithpal-Sooriya/ts-implicit-pick no-readme %}
Partial is great if you want to create an object with some of the values from a given interface. However when the object is used (via property access or through some consuming type), the object still is a Partial
- as in all properties are optional, even if you have provided a value.
In these cases, what we really want is a Pick
ed object - an object with the properties we want "picked" out of the original interface.
Pick is perfect, it gives us the exact strict type that we want, however (as shown) it is very verbose.
For each prop we want, we need to write it for the type
as well as for the object.
For small objects, this might not be much of an issue - however this can become very large the more props we want.
So now lets design an implicit pick!
Here is the design of the initial implicit Pick.
const buildImplicitPickVer1 =
<T>() =>
<K extends keyof T>(props: Pick<T, K>): Pick<T, K> =>
props;
// Usage
const pickProduct = buildImplicitPickVer1<Product>();
const implicitProduct = pickProduct({ id: '1', ... })
Breakdown:
<T>() =>
- The is a factory function part that allows you to build a pick on whatever type you provide it.
<K extends keyof T>
- We have a generic type
K
that is constrained to the type given in the factory.
- We have a generic type
(props: Pick<T, K>): Pick<T, K>
- this parameter gets inferred as the developer types in the keys of their object.
- Invalid keys will give us an error (since does not match the Generic type)
- Invalid values for the key will give us an error, since it won't match the
Pick
ed object values.
This is exactly what we want - a type-safe implicit pick! Refactored changes (renaming/removing) on the interface will propagate through to the objects too!
Well... after some usage I found that it didn't really give a good Developer Experience (DX).
IntelliSense/auto-complete (via CTRL + SPACE
) doesn't give us any useful information on what props we can use.
Only once we start typing do we get errors if a key does not match the interface, we aren't able to get a list of all keys that we can use.
This is because our parameter type in our factory function Pick<T, K>
relies on keys given. Lets fix that!
Here is the the solution:
const buildImplicitPick =
<T>() =>
<K extends keyof T>(props: Partial<T> & Pick<T, K>): Pick<T, K> =>
props;
The small change that made the huge difference is the intersection type Partial<T> & Pick<T, K>
- The
Partial<T>
give us the ability to get back our auto complete for keys. - Intersecting is with the Pick
& Pick<T, K>
ensures that we get the correct type for our key.
Intersection above means that we take only the props/types that match in both types given.
type A = { a: number | undefined }
type B = { a: number }
type C = A & B; // will be { a: number } since that is what both types above have.
Whats awesome is that we can see the IntelliSense working in real time!
When we CTRL + SPACE
to see what props are available, they are all optional because of the Partial
.
But as soon as we select a property to use, it becomes required:
- because the Generic
K
keys are updated; - subsequently so is the
Pick<T, K>
- and finally the intersection
Partial<T> & Pick<T, K>
enforces are type to be required.
Above shows how to write a type-safe, refactor-safe implicit Pick function with useful IntelliSense information.
The function itself is rather simple, but the main takeaway for me is to try/test out different type implementations to provide better IntelliSense information & better developer experience (DX).