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,
};
@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
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>
>;
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.
@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;
}
Most helpful comment
And if you prefer to pass an array/tuple instead of an object in
Keys
, you can use this other alternative:See it live