Few things we need to take into account before we proceed:
- As far as I understood,
key
argument should represent only Option
value
- This expression
({ ...object, [key]: value }))
in TS always returns {[prop:string]: Value}
indexed type instead of expected Record<Key, Value>
object[key]
should be treated as an Option
value inside of function scope.
Let's start from the first statement 1)
In order to assure function scope that key
argument represents Option
value, you need to do this:
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.
GetOptional
- iterates through each key and checks whether value which represents this key is a subtype of O.Option<unknown>
or not. If it is - it returns Prop
name, otherwise - returns never.
Values
- obtains a union of all values in object. Hence GetOptional<Person>
returns pet
, because this is a key which represents Option
value.
As for the second statement 2)
I have provided helper 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)
:
Then, we need to represent filtering in a type 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
FilterOption
- iterates through object and checks whether Prop
is a Key
which in turn represents Option
value. 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
In order t make it composable, just get rid of type assertions and FilterOption
:
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") };
type Values<T> = T[keyof T];
type GetOptional<Obj> = Values<{
[Prop in keyof Obj]: Obj[Prop] extends O.Option<unknown> ? Prop : never;
}>;
const extendObj =
<Obj, Key extends PropertyKey>(obj: Obj, key: Key) =>
<Value,>(value: Value) =>
({ ...obj, [key]: value } as Omit<Obj, Key> & Record<Key, Value>);
const filter = <OptionValue, Obj, Key extends GetOptional<Obj>>(
obj: Obj & Record<Key, O.Option<OptionValue>>,
key: Key
) => pipe(obj[key], O.map(extendObj(obj, key)));
const maybePersonWithPet3 = filter(person, "pet");
const maybePersonWithPet4 = pipe(
O.some(person),
O.chain((person: Person) => filter(person, "pet"))
);