简体   繁体   English

一般过滤 fp-ts 选项属性并提取值

[英]Generically filtering on an fp-ts Option property and extracting the value

I often find myelf implementing the following pattern when using fp-ts:我经常发现自己在使用 fp-ts 时实现了以下模式:

interface Person {
    id: number;
    pet: O.Option<'dog' | 'cat'>;
}

const person: Person = { id: 1, pet: O.some('dog') };

// simplest case:
const maybePersonWithPet = pipe(
    person.pet,
    O.map(pet => ({ ...person, pet })),
);

// very often slightly more cumbersome:
const maybePersonWithPet2 = pipe(
    O.some(person),
    O.filterMap(p =>
        pipe(
            p.pet,
            O.map(pet => ({ ...p, pet })),
        ),
    ),
);

console.log(maybePersonWithPet);
console.log(maybePersonWithPet2);
// { _tag: 'Some', value: { id: 1, pet: 'dog' }

So this is an option filter but it's on a nested option property, where the value of the nested property is extracted.所以这是一个选项过滤器,但它位于嵌套选项属性上,其中嵌套属性的值被提取。 I would like to generalise this, so I thought to write a function that I could call as follows:我想概括这一点,所以我想写一个 function,我可以调用如下:

function filterAndExtractOption<O, K extends keyof O>(object: O, key: K): O.Option<Omit<O, K> & { K: any }> {
    return pipe(
        object[key] as any,
        O.map(value => ({ ...object, [key]: value })),
    ) as any;
}

const maybePersonWithPet3 = filterAndExtractOption(person, 'pet');
console.log(maybePersonWithPet3);

const maybePersonWithPet4 = pipe(
    O.some(person),
    O.chain(p => filterAndExtractOption(p, 'pet')),
);
console.log(maybePersonWithPet4);

What would the correct type definition for the filterAndExtractOption function be? filterAndExtractOption function 的正确类型定义是什么? I need to pass an object, a property key which must be the key for an Option<A> and I also need to extract the A type.我需要传递一个 object,一个属性键,它必须是Option<A>的键,我还需要提取A类型。

I also am wondering if there's a canonical and succinct way of doing this with fp-ts?我还想知道使用 fp-ts 是否有一种规范和简洁的方法来做到这一点?

Few things we need to take into account before we proceed:在我们继续之前,我们需要考虑一些事情:

  1. As far as I understood, key argument should represent only Option value据我了解, key参数应该只代表Option
  2. This expression ({...object, [key]: value })) in TS always returns {[prop:string]: Value} indexed type instead of expected Record<Key, Value> TS 中的这个表达式({...object, [key]: value }))总是返回{[prop:string]: Value}索引类型而不是预期的Record<Key, Value>
  3. object[key] should be treated as an Option value inside of function scope. object[key]应被视为 function scope 内部的Option值。

Let's start from the first statement 1)让我们从第一个陈述开始1)

In order to assure function scope that key argument represents Option value, you need to do this:为了确保 function scope key参数代表Option值,您需要这样做:

type GetOptional<Obj> = Values<{
  [Prop in keyof Obj]: Obj[Prop] extends O.Option<unknown> ? Prop : never;
}>;

const filter = <
  OptionValue,
  Obj,
  Key extends GetOptional<Obj>
>(
  obj: Obj & Record<Key, O.Option<OptionValue>>,
  key: Key
) =>
  pipe(
    obj[key],
    O.map((value) => extendObj(obj, key, value))
  );

Please see my article and SO answer for more details and context.请参阅我的文章SO 答案以获取更多详细信息和上下文。

GetOptional - iterates through each key and checks whether value which represents this key is a subtype of O.Option<unknown> or not. GetOptional - 遍历每个键并检查代表该键的值是否是O.Option<unknown>的子类型。 If it is - it returns Prop name, otherwise - returns never.如果是 - 它返回Prop名称,否则 - 永远不会返回。

Values - obtains a union of all values in object. Hence GetOptional<Person> returns pet , because this is a key which represents Option value. Values - 获得 object 中所有值的并集。因此GetOptional<Person>返回pet ,因为这是代表Option值的键。

As for the second statement 2)至于第二个陈述2)

I have provided helper function:我提供了帮手function:


const extendObj = <Obj, Key extends keyof Obj, Value>(
  obj: Obj,
  key: Key,
  value: Value
) => ({ ...obj, [key]: value }) as Omit<Obj, Key> & Record<Key, Value>;

As for the third statement 3) :至于第三个陈述3)

Then, we need to represent filtering in a type scope.然后,我们需要用类型 scope 表示过滤。

type InferOption<Opt> = Opt extends O.Some<infer Value> ? Value : never;

type FilterOption<Obj, Key extends GetOptional<Obj>> = {
  [Prop in keyof Obj]: Prop extends Key ? InferOption<Obj[Prop]> : Obj[Prop];
};

InferOption - extracts value from Option InferOption - 从Option中提取值

FilterOption - iterates through object and checks whether Prop is a Key which in turn represents Option value. FilterOption - 遍历 object 并检查Prop是否是一个Key ,而 Key 又代表Option值。 If yes - extracts option value, otherwise - returns non modified value.如果是 - 提取选项值,否则 - 返回未修改的值。

Let's put it all together:让我们把它们放在一起:

import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";

interface Person {
  id: number;
  pet: O.Option<"dog" | "cat">;
}

const person: Person = { id: 1, pet: O.some("dog") };

const extendObj = <Obj, Key extends keyof Obj, Value>(
  obj: Obj,
  key: Key,
  value: Value
) => ({ ...obj, [key]: value }) as Omit<Obj, Key> & Record<Key, Value>;

type Values<T> = T[keyof T];

type InferOption<Opt> = Opt extends O.Some<infer Value> ? Value : never;

type FilterOption<Obj, Key extends GetOptional<Obj>> = {
  [Prop in keyof Obj]: Prop extends Key ? InferOption<Obj[Prop]> : Obj[Prop];
};

type GetOptional<Obj> = Values<{
  [Prop in keyof Obj]: Obj[Prop] extends O.Option<unknown> ? Prop : never;
}>;

const filter = <
  OptionValue,
  Obj,
  Key extends GetOptional<Obj>
>(
  obj: Obj & Record<Key, O.Option<OptionValue>>,
  key: Key
) =>
  pipe(
    obj[key],
    O.map((value) => extendObj(obj, key, value))
  ) as FilterOption<Obj, Key>;

const maybePersonWithPet3 = filter(person, "pet");

maybePersonWithPet3.pet; // "dog" | "cat"

Playground操场

So captain-yossarian from Ukraine gave me an answer that worked, but the return type in his solution is rather hard to read.所以来自乌克兰的 captain-yossarian给了我一个有效的答案,但是他的解决方案中的返回类型很难阅读。 Inspired by his answer, I managed to get some help from another person on Discord, and now have a more readable solution.受他回答的启发,我设法从 Discord 上的另一个人那里得到了一些帮助,现在有了一个更具可读性的解决方案。 I've also extended the function to take more than one key:我还扩展了 function 以使用多个密钥:

type Values<T> = T[keyof T];

type GetOptional<Obj> = Values<{
    [Prop in keyof Obj]: Obj[Prop] extends O.Option<unknown> ? Prop : never;
}>;

const sequenceProps = <A, Key extends GetOptional<A>>(
    a: A,
    ...keys: Key[]
): O.Option<{ [K in keyof A]: K extends Key ? (A[K] extends O.Option<infer B> ? B : A[K]) : A[K] }> => {
    const out = {} as any;
    let allSome = true;
    for (const key in a) {
        if (keys.includes(key as any)) {
            const value = a[key] as any;
            if (O.isSome(value)) {
                out[key] = value.value;
            } else {
                allSome = false;
                break;
            }
        } else {
            out[key] = a[key];
        }
    }
    return allSome ? O.some(out) : O.none;
};

I can call the function as follows:我可以拨打 function,如下所示:

type PetType = 'dog' | 'cat';
type VehicleType = 'bicycle' | 'car';
interface Person {
    id: number;
    pet: O.Option<PetType>;
    vehicle: O.Option<VehicleType>;
}
const person: Person = { id: 1, pet: O.some('dog'), vehicle: O.some('bicycle') };

const maybePersonWithPetAndVehicle = sequenceProps(person, 'pet', 'vehicle');

... and the return type is clean; ...并且返回类型是干净的; when I hover over the variable, I get:当我 hover 超过变量时,我得到:

const maybePersonWithPetAndVehicle: O.Option<{
    id: number;
    pet: PetType;
    vehicle: VehicleType;
}>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM