Flow: $Pick utility type

Created on 11 Feb 2017  路  20Comments  路  Source: facebook/flow

Hi there,

I've just came across with the need of defining an object type using a subset of another, or in other words, use some props definition of a certain object type and intersect this result (a reduced object type) with another set of props definitions to define a new type.

I used to do this a lot with PropTypes using pick utility from lodash, but we've recently moved to flow and I really miss this functionality, so I was wondering if you have ever thought of the possibility of adding a $Pick utility type to be able to do something like the following:

// @flow

type O1 = {
  o1k1: string,
  o1k2: number,
  o1k3: boolean,
};

// o1k1 and o1k2 mandatory
type O2 = $Pick<O1, o1k1 & o1k2> & {
  o2k1: () => void,
  o2k2: Date,
};

// o1k1 and o1k2 optional
type O2 = $Pick<O1, ?o1k1 & ?o1k2> & {
  o2k1: () => void,
  o2k2: Date,
};

Right now I partially dealing with this using $Shape like the following, but this doesn't restrict additional props I don't want to, neither it let me mark any of them as required

// @flow

type O1 = {
  o1k1: string,
  o1k2: number,
  o1k3: boolean,
};

type O2 = $Shape<O1> & {
  o2k1: () => void,
  o2k2: Date,
};
feature request

Most helpful comment

And if you prefer to pass an array/tuple instead of an object in Keys, you can use this other alternative:

const toObject = keys => keys.reduce((object, key) => {
  object[key] = undefined;

  return object;
}, {});

type Pick<Origin: Object, Keys: $ReadOnlyArray<$Keys<Origin>>> = $ObjMapi<
  $Call<typeof toObject, Keys>,
  <Key>(k: Key) => $ElementType<Origin, Key>
>;

See it live

All 20 comments

@mgtitimoli You can do this, albeit with a tiny bit more typing, using the $PropertyType utility:

type O1 = {
  o1k1: string,
  o1k2: number,
  o1k3: boolean,
};

type O2 = {
  o2k1: () => void,
  o1k1: $PropertyType<O1, 'o1k1'>,
  o1k2: $PropertyType<O1, 'o1k2'>
};

type O3 = { 
  o2k1: () => void,
  o1k1?: $PropertyType<O1, 'o1k1'>,
  o1k2?: $PropertyType<O1, 'o1k2'>
}

Does that work for your use-case? Or maybe you can elaborate about the context and why you need this and we can try to find a creative solution.

Hi @asolove,

I'm currently doing the way you described. What I'm proposing is to simplify that construct as follows:

type O1 = {
  o1k1: string,
  o1k2: number,
  o1k3: boolean,
};

type O2 = $Pick<O1, ["o1k1", "o1k2"]> & {
  o2k1: () => void,
};

(I hope using an array syntax would have simplified the idea)

The alternative I originally wrote would allow specifying optional entries (by prepending ? to them), but I've just realized that construct can be done as follows:

type O1 = {
  o1k1: string,
  o1k2: number,
  o1k3: boolean,
};

type O2 = $Pick<O1, ["o1k1"]> & $Shape<$Pick<O1, ["o1k2", "o1k3"]>> & {
  o2k1: () => void,
};

I would also like to mention that a complementary utility type would be super useful: $Omit

(It's worth noting that this proposal is heavily inspired by lodash pick and omit)

This would be super cool, especially if we could pass to $Pick/$Omit $Keys's output.

Something like:

const omit = <T: Object, K: $Keys<T>>(keys: K[], obj: T): $Omit<T, K> =>
  Object.keys(obj)
    .filter(key => keys.indexOf(key) === -1)
    .reduce((result, key) => {
      result[key] = obj[key]
      return result
  }, {})

I'd also really like to be able to do this -- I'm looking for the equivalent of typescript's Pick<T, K> (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html), which would let us correctly type things like _.pick (compare our types to TypeScript's types).

This goes along with what I just posted with #5767 I think the real goal here is something which is capable of mapping tuples to objects in general?

Looking for an $Omit style feature. I'm thinking it should be useful when wrapping a component with some default props. Maybe something like this exists and I can't find it? See example:

import InnerComponent, { type Props } from 'inner-component'

type PropsExcludingDefaults = ???

export const DefaultInnerComponent: React$StatelessFunctionalComponent<PropsExcludingDefaults> = (props) => (
  <InnerComponent
    {...defaults()}
    {...props}
  />
)

@TSMMark There is $Rest.

Hi @TSMMark,

You might find #5006 useful...

Have a look also to $Call utility type (you can get the result of defaults using this one).

Thanks! $Rest worked great for pulling out the types. Now I need to figure out how to fix flow when merging them back together.

// $FlowFixMe object literal. Inexact type is incompatible with exact type
<InnerComponent
  {...defaults()}
  {...props}
/>

@TSMMark looks like another issue - see #5551

We can replicate $Omit non dynamic functionallity by doing the following:

type T0 = {
  k0: string,
  k1: number,
  k2: boolean
};

// this would be the same as type T1 = $Omit<T0, ["k0", "k1"]>
type T1 = $Diff<T0, {
  k0: *,
  k1: *
}>;

I finally came up with a solution :tada::

type Pick<Origin: Object, Keys: Object> = $ObjMapi<
  Keys,
  <Key>(k: Key) => $ElementType<Origin, Key>
>;

Exactness can be controlled by passing an exact object in Keys

See it live

And if you prefer to pass an array/tuple instead of an object in Keys, you can use this other alternative:

const toObject = keys => keys.reduce((object, key) => {
  object[key] = undefined;

  return object;
}, {});

type Pick<Origin: Object, Keys: $ReadOnlyArray<$Keys<Origin>>> = $ObjMapi<
  $Call<typeof toObject, Keys>,
  <Key>(k: Key) => $ElementType<Origin, Key>
>;

See it live

Thanks @mgtitimoli , this actually makes sense and seems to work.
I had only a small issue with it - I can specify any field that I want... const c1: T0 = {k0: true, a: 2}; should actually give an error, because a is not a known field.

Array/tuple variant does not work if you export and import Pick type since Flow v0.86.

@alexandersorokin Same in v0.92.1. Types report any(implicit) although validation of the tuple still works as expected (they have to be valid keys in Origin).

Fixed mgtitimoli version for keys provided in array

 type Pick<Origin: Object, Keys: $ReadOnlyArray<$Keys<Origin>>> = $ObjMapi<
    Origin,
    <Key>(k: Key) => $ElementType<Origin, Key>
  >;

Import should work fine and now Pick is highlighting undefined keys in type declaration as well as when trying to call a not existing property in your code.

Check on flow try

@AndrzejSala, your version includes all fields of type of original object and does not remove anything.

Pick<Obj, Keys> utility type should remove from Obj every field that missing in Keys.

See tryflow. There should be error at line 25.

I'm using

export type Pick<FromType, Properties> =
  $Exact<$ObjMapi<Properties, <K, V>(k: K, v: V) => $ElementType<FromType, K>>>;


export function pick<FromType, Properties>(
  from: FromType,
  props: Properties,
): Pick<FromType, Properties> {
  const result: any = {}
  if (typeof from !== 'object' || from === null) return result
  const keys = Object.keys(props)

  for (let i = 0, n = keys.length; i < n; i++) {
    const key = keys[i]
    result[key] = from[key]
  }
  return result
}

(as suggested above).

Usage as function:

const original = {foo: 'Hello', bar: 'World', baz: 'You'}
const picked = pick(original, {foo: 0, baz: 0})

The type of the values in the second argument don't matter, I'm using 0 just because it's easy to type and isn't too noisy, but you coud use something like null too.

Usage as as a type

type Picked = Pick<Original, {foo: any, baz: any}>

This is obviously not as nice as your usual pick(.., ['foo', 'baz']), the {foo: 0, baz: 0} does look weird, but at least it seems to currently work really well with Flow. Flow correctly infers the type to be {|foo: string, baz: string|}. And @alexandersorokin your example gives an error too with this type def like it should.

@noppa Thanks for sharing your code, appears to work well for me! I tweaked it a little big to make sure the the values of the pick object are always 0. I know it doesn't matter, but helps to enforce consistency in usage.

export type Pick<FromType, Properties: { [string]: 0 }> = $Exact<
  $ObjMapi<Properties, <K, V>(k: K, v: V) => $ElementType<FromType, K>>
>;

export function pick<FromType, Properties: { [string]: 0 }>(
  from: FromType,
  props: Properties
): Pick<FromType, Properties> {
  const result: any = {};
  if (typeof from !== 'object' || from === null) return result;
  const keys = Object.keys(props);

  for (let i = 0, n = keys.length; i < n; i++) {
    const key = keys[i];
    result[key] = from[key];
  }
  return result;
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Beingbook picture Beingbook  路  3Comments

mjj2000 picture mjj2000  路  3Comments

cubika picture cubika  路  3Comments

iamchenxin picture iamchenxin  路  3Comments

ctrlplusb picture ctrlplusb  路  3Comments