The Value of a Pick<>
Tags: typescript, javascript, todayilearned, programming
Published: 2022-01-06 23:43:56

Recently I was writing an API handler that retrieves an object and then returns a partial copy with only the object properties “picked” by the caller.  Pretty standard stuff... until TypeScript intervened.  

In Typescript, we have the generic utility type `Pick<T, K>`.  It’s super handy.  `Pick<T, K>`  returns a **type** with only some of the properties (described by the string union `K`) of the original object’s type (`T`). Since `Pick` is a Typescript utility *type*, it only acts on the *types* (not the *values* of the object).  So all Pick’s hard work gets effectively [erased at runtime](https://github.com/Microsoft/TypeScript/wiki/FAQ#what-is-type-erasure) and doesn’t alter the actual object being returned. 😔    

How do we code this same Pick-like functionality in the world of runtime *values*, while still preserving the type safety of TypeScript?  My investigation into this seemingly simple question, led me to several interesting discoveries and surprises about TypeScript.

## Our musical example
To illustrate my example, let’s call on one of the most  [inspiring bands in progressive acoustic music](https://www.punchbrothers.com/) :
```typescript
type PunchBrother = {
  name: string;
  instrument: string;  
  leadSinger: boolean;
};
const mandolinist = {
  name: 'Chris Thile', // virtuoso mandolinist
  instrument: 'mandolin', 
  leadSinger: true,
};
```

Our aim is to write a function that returns just a few properties of the `mandolinist` object:
```typescript
function punchBrotherPick(musician: PunchBrother, keys: Array<keyof PunchBrother>): Partial<PunchBrother> {
	// ... ??? ...
	return partialBrother;
}
```
Note that we define the return type using Typescript’s `Partial<T>` utility type since we may only be selecting *some* of the properties of the object (and thus omitting others).

We'll then call our function like:
```typescript
const mandolinistName = punchBrotherPick(mandolinist, ['name']);

mandolinistName.name === 'Chris Thile'; // true
mandolinistName.instrument === undefined; // true, type is Partial<PunchBrother>
mandolinistName.faveCocktail; // type error, 'faveCocktail' does not exist on Partial<PunchBrother>
```

🎵  [My, oh my.](https://youtu.be/_H2o217hqJU?t=57)  What a wonderful day we’re having… 🎵

## Destructuring a dynamic list of properties
Quick searches on StackOverflow all suggest the elegant approach of object destructuring with rest parameters:  
```typescript
const { key1, key2, ...withoutKey1Key2 } = origObj;
```
Ah, yes.  I love that destructuring syntax for its simple clarity. `withoutKey1Key2` now contains all properties in `origObj` minus `key1` and `key2`.   

Note that this one-liner more closely mimics Typescript’s `Omit<T, K>` since `withoutKey1Key2` now omits `key1` and `key2`.  But we can quickly spread the `key1` and `key2` properties back into a new object to get the functionality similar to Pick.  
```typescript
const { key1, key2, ...rest } = origObj;
const onlyKey1Key2 = { key1, key2 };
```

Unfortunately, this approach won’t work here.  Destructuring only works when the number of extracted properties is static and known at compile time.  In our more general case of picking an arbitrary, dynamic array of properties (specified by the caller as an array of keys), destructuring isn’t possible  ([See this SO article](https://stackoverflow.com/questions/54749242/delete-a-dynamic-key-from-a-typescript-object)) .  

A couple asides:
* Note that you *can* destructure with a dynamic key *name* via `{ [keyNameVar]: var, …rest}`.  Very hip!
* The problem here is specifying an arbitrary *quantity* of these dynamic keys.  You’d need a meta-programming way of specifying the destructure syntax.  If that’s possible in Javascript I’d love to hear about it!


## Clone then mutate
Another option is to first clone the object (using your clone method of choice), then selectively remove the properties we don’t need via Javascript’s `delete`.   
```typescript
const partialThile: Partial<PunchBrother> = Object.assign({}, mandolinist); // cloned object
delete partialThile.instrument;
delete partialThile.leadSinger;
```

It’s nice to know that `delete` is sound with regards to types.  In order for a property to be deleted, Typescript requires that the property must already be optional on the object. Well done, TS! 

But I’m not thrilled with this approach, as it is more analogous in spirit to Typescript’s `Omit`.  We have to clone the entire object, then remove the fields that we **don’t** want to include.  This approaches the idea of `Pick` from its inverse.  

Interestingly, `Omit` itself is defined in TS ([/lib/es5.d.ts](https://github.com/microsoft/TypeScript/blob/cec2fda9a53620dc545a2c4d7b0156446ab145b4/lib/lib.es5.d.ts)) using Pick and Exclude: 
```typescript
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
``` 

But let’s dig deeper into this approach as there are some other potential problems.

### Iterating over keys of an object
At runtime, all properties of an object are visible, even ones that have been “hidden” from TypeScript via type narrowing. We might iterate over the keys of a PunchBrother object expecting to see just our 3 PunchBrother properties, but actually see additional properties.  Consider this:     
```typescript
// Punch Brothers bassist
const paulKowert = {
	name: 'Paul Kowert',
	instrument: 'bass',
	leadSinger: false,
	otherBands: ['Hawktail'] // field not declared on PunchBrothers type
}
const punchPaul: PunchBrother = paulKowert; // type narrowing

punchPaul.otherBands; // Type Error: Property 'otherBands' does not exist on type 'PunchBrother'.
```

As expected, TypeScript errors if we attempt to access `punchPaul.otherBands`.  But at runtime, if we attempt to iterate over the keys of `punchPaul`, we will see the `otherBands` property as well as the 3 PunchBrother properties. Type narrowing like this only happens at compile time; these types are completely erased from the runtime Javascript.

The TypeScript designers made the decision to type the return value of `Object.keys` and `for..in` as `string` rather than `keyof obj` for this reason: the compiler just can’t be certain there aren’t other properties on the object.  (See lots of great info and links on this [StackOverflow post](https://stackoverflow.com/questions/59233965/typescript-element-implicitly-has-type-any-with-for-in-loops)).

We can get *some* type safety by using the `for…in` syntax.  If we declare the key variable inside the `for..in` the key will be of type string. But we can declare our `key` variable prior to the `for..in` and include a type annotation:  
```typescript
let key: keyof PunchBrother;
for (let key in punchPaul) { ... } // type of key is still `keyof PunchBrother`
```
Curiously (?), we can annotate our type with a narrower type here (`keyof PunchBrother` is narrower than `string`) and not receive a TypeScript error when using the variable in the `for..in`.

This satisfies the TypeScript compiler, but it is not sound.  In our `punchPaul` example, the runtime value of `key` can still be `otherBands` which is not a member of the union `keyof PunchBrother`.  

The use of `for..in` this way is fine if we know that our object exactly matches the type and doesn’t possesses any properties beyond those declared in the type. But if our object is narrowed from another type, as in the case above, the type declaration for `key` may not be sound. 

Given the potential unsoundness of iterating over object keys, as well as the semantic backwardness of a “clone then mutate” approach, let’s look at a better solution. 

## Selectively copy properties
The more natural approach to our initial issue is to begin with an empty object (`{}`) and selectively copy the requested properties from the source object.  (This is the approach used by the Just utility library’s [just-pick](https://github.com/angus-c/just#just-pick).)

Here’s the naive code:
```typescript
const thileInstrument: Partial<PunchBrother> = {}; // must be Partial
const fields: Array<keyof PunchBrother> = ['instrument'];

fields.forEach((key) => {
  thileInstrument[key] = thile[key]; // Error: Type 'string | boolean' is not assignable to type 'undefined'.
});
```

And now we reach the most surprising hurdle of this article: copying fields between 2 objects. Our innocent little code: `target[key] = src[key]` yields a type error: `Type 'string | boolean' is not assignable to type 'undefined'.`  

Huh?  Isn’t it self-evident that this is type-safe?  The objects are the same type, we’re using the same keys, shouldn’t all the types match?  And equally surprising, why is the type of the left-hand-side (target[key]) 'undefined'?

Let’s break this down from the perspective of the TypeScript compiler.  For each iteration of the loop, there is a single key.  But at compile time, Typescript doesn’t know *which* key. So it also can’t know the type of the property in the object: `srcObj[key]`.   

For clarity, let’s introduce a temporary variable for the right-hand side (RHS) value:
```typescript
fields.forEach((key) => {
	const rhs = thile[key]; // inferred type is: 'string | boolean'
  thileInstrument[key] = rhs; // Error!
});
```

### Type of the RHS
The type of the right-hand side in the assignment is the **union** of all possible property types in the object.   

To quickly unpack this indexed access type: 
* The type of `key` is `’name’ | ‘instrument’ | ‘singer’`.  
* So the type of `rhs` is `PunchBrother[’name’ | ‘numInstruments’ | ‘singer’]` 
* After distributing out the string union: `PunchBrothers[‘name’] | PunchBrothers[‘instrument’] | PunchBrothers[‘singer’]`
* This simplifies to: `string | boolean`

### Type of the LHS
While the type of the RHS feels immediately intuitive (the union of all property types), the type of the left-hand side of the assignment is somewhat surprising.  

TypeScript resolves the type of a left-hand side of an assignment to be the **intersection** 🤯 of the types of all properties on the object.  (Let that sink in for a minute...) This is a deliberate (though unfamiliar to me!) decision by the TypeScript designers to make assignments as sound as possible.   For more details see [this TypeScript PR discussion](https://github.com/microsoft/TypeScript/pull/30769) and this [excellent post about “unexpected intersections”](https://fettblog.eu/typescript-unexpected-intersections/)).  

🎵 It’s all  [part of the plan](https://www.youtube.com/watch?v=y64KTlk6N_s) 🎵.

The basic intuition is that the type of LHS should resolve to the set of types that can safely be assigned to. This type set is represented by the *intersection* of all the property types.   When the intersection of property types is a single concrete type, the type-safety of this assignment is clear.  For example, if the object type was the simpler: `Record<K, string>` then the intersection of `string & string & string`would be `string` and the assignment above would be type-safe.  

But in our case the type of the LHS is: `’string & number & undefined’` (Recall that our LHS is of type `Partial<PunchBrother>` so each property may also be `undefined`.)  

As `string` and `number` do not overlap, this intersection should resolve to `never`. Or in our specific case, where our left-hand side object is a `Partial<>`, this may actually resolve to  `undefined`.  Regardless, the types in the LHS and RHS aren’t compatible.  

(🎵  [I'm a magnet](https://youtu.be/iXE_K2Kpoqc?t=633) , And you're a magnet, And we're pushing each other away. 🎵)

### A TypeScript assignment solution
Given the type incompatibility between the LHS and RHS of the assignment, we need a different approach.  The problem is that TypeScript only knows the type of either side as `T[K]`, where `K` is the *set of all keys*.  So intuitively, the solution is to explicitly *freeze* (technically called “bind”) the specific key for the LHS and RHS on each iteration of the loop.  Let’s call a generic helper function for each different key value:
```typescript
function copyField<T>(target: T, src: Readonly<T>, key: keyof T): void {
    target[key] = src[key];
}
``` 

TypeScript is perfectly happy with this assignment.  It now knows the objects are the same type, the key is a property on their type, and we’re accessing the same property in both objects.   

> In order for TypeScript to fully resolve the specific LHS and RHS type,  you might think we’d need to also make the key type explicit like so: `function copyField<T, K extends keyof T>(target: T, src: Readonly<T>, key: K)`. But simply introducing the single `T` generic parameter is sufficient for TypeScript to infer the specific type for the LHS and RHS.  Thus, we are able to avoid introducing a second generic parameter which would otherwise violate  [The Golden Rule of Generics](https://effectivetypescript.com/2020/08/12/generics-golden-rule/) .  
🎵 Heaven’s a  [julep on the porch](https://www.youtube.com/watch?v=aerar7MjtFM) 🎵! 

Adding this utility function to the loop, here’s our full type-safe solution.
```typescript
const thileInstrument: Partial<PunchBrother> = {};
const fields: Array<keyof PunchBrother> = ['instrument'];

function copyField<T>(target: T, src: Readonly<T>, key: keyof T): void {
    target[key] = src[key];
}
fields.forEach((key) => {
	copyField(thileInstrument, thile, key);  // TypeScript success!
});
```

Depending on the situation, it might make sense to inline this 1-line `copyField()` function as a quick TypeScript IIFE. But that risks further obfuscating the solution to our seemingly very simple situation.

> One more aside: My initial TypeScript intuition was to search for a runtime / type-guard solution, along the lines of `if typeof target[key] === typeof src[key]`.  That’s often the solution to runtime type issues.  But alas that approach doesn’t generalize for the common case of nested objects, since `typeof` a nested object just returns `object`. There’s probably a solution involving recursively checking types of nested properties.  But at that point, our 1-line `copyField()` utility function is looking pretty nice!

## Ok, but is this worth it?
In general, the aim of TypeScript is to provide safety and confidence in parts of our code where we might realistically make a mistake and introduce a bug.  

Part of TypeScript’s allure lies in the fact that programmers are rarely good at knowing where they’re “realistically” likely to make a mistake — or where future maintainers might introduce a compounding mistake.  In complicated code with function calls spanning many files, this compile-time static validation is invaluable.  But is a simple copying of values between 2 objects of the same type one of those areas?  

Couldn’t we have just asserted type `any` on the right-hand side of the assignment and been done awhile ago?  (or suppress the error via `// @ts-ignore`) ? 

Isn’t the added complexity (over-engineering?!) of this code more likely to introduce future confusion than the added type safety of the original assignment?  We’re introducing an additional function (or IIFE) with a TypeScript generic, and we’re ( 😱 eek! 😱) mutating one of our function arguments. Is it worth all that additional complexity?

It’s up to you and your team.  But this utility function does provide the additional confidence that: 
* both the source and the target object are the same type, 
* the key is valid on the objects,
* we’re copying the same key (and thus the same type) on both sides of the assignment operator.  

Ultimately, I think this falls into the gray area of a static tool like TypeScript.  If your code is self-evident and isolated, then the additional cognitive overhead might not be necessary.  But used with complex objects that might be subtypes, I can see a value in this little one-liner.  

What do you think?  Was this a worthwhile use of TypeScript generics?  I'd love to hear your thoughts in the comments below.