io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

[discussion] something similar to TS Pick<T>

Open zerkms opened this issue 7 years ago • 29 comments

Do you want to request a feature or report a bug? A feature

With typescript it's possible to have

interface Foo {
    a: string;
    b: string;
}

type Bar = Pick<Foo, 'a'>;

// identical to
/*
interface Bar {
    a: string;
}
*/

Is it technically possible to have something similar in io-ts that looks like

const FooT = t.interface({
    a: t.string,
    b: t.string,
});

const BarT = t.pick(FooT, t.literal('a'));

?

zerkms avatar Mar 05 '19 02:03 zerkms

You can use a "standard" pick function

declare function pick<O, K extends keyof O>(o: O, keys: Array<K>): Pick<O, K>

const BarT = t.type(pick(FooT.props, ['a']))

gcanti avatar Mar 05 '19 04:03 gcanti

Indeed.

2 points though:

  1. pick should be implemented somewhere
  2. Typescript's Pick<Foo, 'a'> second argument type checks that it makes sense. Pick<Foo, 'something'> wouldn't pass a type check. pick(FooT.props, ['a']) would accept anything as a second argument.

Hence why I started this discussion: given io-ts already implements some base generic TS types like Record<> and Partial<>, would it make sense to provide an implementation for the Pick<> as well that carries its semantics?

zerkms avatar Mar 05 '19 20:03 zerkms

You can use a "standard" pick function

I also don't think that that solution works for intersection codecs, as far as I can tell. t.intersection([t.type({foo: t.string}), t.partial({bar: t.number})]) doesn't seem to have a props field, but i still want to pick from it as i can with merged TypeScript interfaces.

osdiab avatar Nov 08 '19 06:11 osdiab

intersection has a types array were you can access the inner types. Then you can use props again.

mlegenhausen avatar Nov 11 '19 07:11 mlegenhausen

Ah, makes sense. That said, it seems kinda silly that I'd have to know the inner implementation of io-ts to achieve this

osdiab avatar Nov 11 '19 08:11 osdiab

That said, it seems kinda silly that I'd have to know the inner implementation of io-ts to achieve this

You don't have to, but you need to understand what an intersection type is. Then you can better reason about why it is the way it is. An intersection does not have to consist only of object like types.

const A = t.intersection([t.string, t.type({ bar: t.number })])

so without the extra layer of types you can not work with an intersection type like this.

mlegenhausen avatar Nov 11 '19 09:11 mlegenhausen

except to execute a pick wouldn't i have to know about the inner types array and props field for type? that seems like a significant amount of undocumented implementation details of the particular library, and while i'm all for knowing it inside and out for your personal edification/power use, i don't see why I'd have to educate the rest of my team on this kind of stuff just so they can implement a pick on their own.

osdiab avatar Nov 11 '19 09:11 osdiab

As for many open source projects the documentation is suboptimal, but types is document here https://gcanti.github.io/io-ts/modules/index.ts.html#intersectiontype-class. Providing additional documentation is always welcome so is new functionality too.

mlegenhausen avatar Nov 12 '19 07:11 mlegenhausen

@mlegenhausen is it my original post still discussed? I'm not sure I understand how intersection has anything to do with Pick<T, K>?

zerkms avatar Nov 12 '19 07:11 zerkms

@zerkms it is related because you can define new interface like types with an intersection, that combine multiple t.interface and t.partial definitions to a new type. For this new type you could also define a pick function as it would work without io-ts.

mlegenhausen avatar Nov 15 '19 07:11 mlegenhausen

@mlegenhausen I'm not sure I'm following: Pick<T, K> where K is a keyof T.

With intersection-based solution you must declare both types of the keys and the values. That's the significant difference: I'd rather infer the value than have to declare it manually.

zerkms avatar Nov 15 '19 07:11 zerkms

@zerkms this is valid typescript

type X = Pick<{ foo: number } & { bar?: string }, “bar”>

With a pick function, I assume this should work:

const x = pick(intersection([type({ foo: number }), partial({ bar: string })]), [“bar”]);

because it’s analogous TypeScript to io-ts code.

osdiab avatar Nov 16 '19 11:11 osdiab

@osdiab would it accept ['any rubbish'] as its second argument?

zerkms avatar Nov 16 '19 11:11 zerkms

Ah I see what you’re saying, but I feel fairly confident it’s possible for it to be inferred with a recursive type.

EDIT: working version at https://github.com/gcanti/io-ts/issues/300#issuecomment-554845184

type KeyOfCodec<Codec extends Any> =
  Codec extends Intersection ? 
    KeyOfCodecs<Codec[“types”]>
    : Codec extends Type ?
      keyof Codec[“props”]
      : something // other cases

// not sure if this is constructed correctly,
// if a fully recursive variadic type can’t
// work properly then it can at least be
// manually specified for a reasonable
// number of array lengths 
type KeyOfCodecs<Array extends Any[]> =
  Array extends [Head, ...Tail] ?
    KeyOfCodec<infer Head> | KeyOfCodecs<infer Tail>
    : never

osdiab avatar Nov 17 '19 01:11 osdiab

Ah for the array this would be relevant?

https://github.com/microsoft/TypeScript/issues/25947#issuecomment-446916897

https://github.com/microsoft/TypeScript/issues/5453#issuecomment-419680547

osdiab avatar Nov 17 '19 02:11 osdiab

This seems to work when I tried it out on my machine for inferring the keys properly, Typescript 3.7.2:

type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
type Tail<T extends any[]> =
 ((...args: T) => never) extends ((a: any, ...args: infer R) => never)
  ? R
  : never

export type KeysOfCodecs<Codecs extends Mixed[]> = {
  recurse: KeyOfCodec<Head<Codecs>> | KeysOfCodecs<Tail<Codecs>>
  end: never
}[Codecs extends [Mixed, ...Mixed[]] ? "recurse" : "end"];

export type KeyOfCodec<Codec extends Mixed> =
  Codec extends IntersectionC<infer Codecs> 
    ? KeysOfCodecs<Codecs>
    : Codec extends TypeC<infer TypeProps>
      ? keyof TypeProps 
      : Codec extends PartialC<infer PartialProps> 
        ? keyof PartialProps
        : never;


export function pick<Codec extends Mixed, Keys extends KeyOfCodec<Codec>>(
  codec: Codec,
  keys: Keys[]
): PickCodec<Codec, Keys> { // PickCodec not implemented yet
    throw new Error("not yet implemented")
}

Not sure how stable that is for TypeScript versions, as the inference of tuples is definitely a feature in flux in TypeScript, wouldn't work for old TypeScript versions for sure

osdiab avatar Nov 18 '19 04:11 osdiab

@gcanti Is the canonical solution still to use a generic pick, or is io-ts going to provide an implementation at some point? Thanks!

VanTanev avatar Feb 25 '20 10:02 VanTanev

is io-ts going to provide an implementation at some point?

@VanTanev No, I don't think so

gcanti avatar Feb 25 '20 17:02 gcanti

is io-ts going to provide an implementation at some point?

@VanTanev No, I don't think so

@gcanti How about adding it to the non-core package io-ts-types?

ivawzh avatar Aug 14 '20 06:08 ivawzh

How about adding it to the non-core package io-ts-types?

@ivawzh I'm not against that, however if the solution is not implementable using the new experimental modules, it's not going to last

gcanti avatar Aug 21 '20 16:08 gcanti

For folks looking to make this work only for a simple t.type (like me), this seems to work reasonably well:

export function pick<P extends t.Props, K extends keyof P>(
  Model: t.TypeC<P>,
  keys: K[],
): t.TypeC<Pick<P, K>> {
  const pickedProps = {} as Pick<P, K>;
  keys.forEach(key => {
    pickedProps[key] = Model.props[key];
  });
  return t.type(pickedProps);
}

Usage:

const PickedModel = pick(Model, ["id", "name"]);
type PickedModel = t.TypeOf<typeof PickedModel>;

mDibyo avatar Nov 17 '20 07:11 mDibyo

And to complement's @mDibyo pick, this works for omit

export function omit<P extends t.Props, K extends keyof P>(
  Model: t.TypeC<P>,
  keys: K[],
): t.TypeC<Pick<P, Exclude<keyof P, K>>> {
  const allKeys = Object.keys(Model) as K[];
  const keesToKeep = allKeys.filter((x) => !keys.includes(x)) as Exclude<
    typeof allKeys,
    typeof keys
  >;
  return pick(Model, keesToKeep);
}

cortopy avatar Jan 27 '21 11:01 cortopy

First, I really appreciate the work on this library. It's pretty epic. I've been evaluating io-ts, and I saw this compatibility chart which made me happy that this was tracking feature parity with TypeScript. omit and pick seem to be a point of divergence, so I'm curious what the philosophy of the project is going forward on maintaining parity.

akutruff avatar Jan 27 '21 22:01 akutruff

@mDibyo Thanks for the pick function.

Noob question: I couldn't use the same pick function for a type created using t.intersection. I'm guessing this is because the type is IntersectionC instead of TypeC. Is there any way to make it work for types created using intersection?

sagarchk avatar Feb 24 '21 05:02 sagarchk

Not at all a noob question @sagarchk - I was trying to figure out the same thing. 😆

Yeah exactly, the pick function will only work with TypeC currently. As far as I can tell, making pick work with t.intersection is pretty hard, and will best be implemented at the library level. There's some discussion about this earlier in this Issue if you're interested in learning more.

For this and other reasons, we have stopped using t.intersection in the project we are working on.

EDIT: Thinking more about this, making pick work with intersection might not be that hard given the property:

pick(intersection(A, B), keys) <=> intersection(pick(A, keys), pick(B, keys))

Someone just has to implement it. 😆

mDibyo avatar Feb 24 '21 15:02 mDibyo

Curious what you do as an alternative? Custom intersection implementation?

On Thu, Feb 25, 2021 at 0:16 Dibyo Majumdar [email protected] wrote:

Not at all a noob question @sagarchk https://github.com/sagarchk - I struggled with the same thing. 😆

Yeah exactly, the pick function will only work with TypeC currently. As far as I can tell, making pick work with t.intersection is pretty hard, and will best be implemented at the library level. There's some discussion about this earlier in this Issue if you're interested in learning more.

For this and other reasons, we have stopped using t.intersection in the project we are working on.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/gcanti/io-ts/issues/300#issuecomment-785147084, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAONU3WWZOGZUD6XYAXJEB3TAUJ4NANCNFSM4G3WS4FA .

-- Omar

osdiab avatar Feb 25 '21 03:02 osdiab

@osdiab Just saw your response.

We haven't found a need to use t.intersection. t.type combined with the pick implementation above has proven enough for defining types for decoding untyped data.

And once the data is typed, we can always just use Typescript intersections.

mDibyo avatar Dec 04 '21 00:12 mDibyo

Full example using implementations above:

import * as t from "io-ts"

export function pick<P extends t.Props, K extends keyof P>(
  Model: t.TypeC<P>,
  keys: K[]
): t.TypeC<Pick<P, K>> {
  const pickedProps = {} as Pick<P, K>
  keys.forEach((key) => {
    pickedProps[key] = Model.props[key]
  })
  return t.type(pickedProps)
}

export function omit<P extends t.Props, K extends keyof P>(
  Model: t.TypeC<P>,
  keys: K[]
): t.TypeC<Pick<P, Exclude<keyof P, K>>> {
  const allKeys = Object.keys(Model.props) as K[]
  const keysToKeep = allKeys.filter((x) => !keys.includes(x)) as Exclude<
    typeof allKeys,
    typeof keys
  >
  return pick(Model, keysToKeep)
}

Almaju avatar Sep 01 '22 00:09 Almaju

There is a TC39 proposal for adding basic pick and omit to JavaScript. Won't change much but might be useful in some way if the proposal makes it through the process. See https://github.com/tc39/proposal-object-pick-or-omit

cyberixae avatar Nov 05 '22 03:11 cyberixae