FP-TS Equivalent of Lodash or Ramda `cond`? - fp-ts

Trying to figure out how to model multiple cases with fp-ts. Unsure whether my mental model of this operation should be different in fp-ts, whether I can't find the right function to use, or such a function doesn't exist.
For reference, https://ramdajs.com/docs/#cond (same signature+behavior in Lodash)
Example cases would be loading/loaded states, or splitting cases on an enum. E.g.,
enum abcd {a,b,c,d}
const getCaseResult = cond([
[is(a), getResultA],
[is(b), getResultB],
[is(c), getResultC],
[is(d), getResultD],
])
getCaseResult('a') // returns result of getResultA('a')

I did a bit of digging into the type definitions for Ramda because I was curious how they were handling the types if in fact the predicate functions were type guards. It turns out they don't use type guards to narrow the types.
See DefinitelyTyped Code.
Incidentally the comment there actually invalidates the type definitions they're giving:
If none of the predicates matches, fn returns undefined.
But the types state it returns an R not R | undefined. I took a crack at implementing cond with equivalent types using fp-ts utilities, because as some commenters pointed out, it doesn't look like fp-ts has a built in equivalent to cond.
Would something like this work for you?
import { reduce } from "fp-ts/lib/Array";
import { alt, some, none, fold, Option } from "fp-ts/Option";
import { pipe } from "fp-ts/lib/function";
type CondPair<T extends any[], R> = [
(...args: T) => boolean,
(...args: T) => R
];
const cond = <T extends any[], R>(pairs: Array<CondPair<T, R>>) => (
...args: T
): R | undefined =>
pipe(
pairs,
reduce(
none,
(prev: Option<R>, [pred, res]: CondPair<T, R>): Option<R> =>
pipe(
prev,
alt(() => (pred(...args) ? some(res(...args)) : none))
)
),
fold(
() => undefined,
(r) => r
)
);
enum abcd {
a,
b,
c,
d
}
const is = <T>(t: T) => (x: unknown) => t === x;
function isResultA(a: abcd) {
return 1;
}
function isResultB(b: abcd) {
return 2;
}
function isResultC(c: abcd) {
return 3;
}
function isResultD(d: abcd) {
return 4;
}
const getCaseResult = cond([
[is(abcd.a), isResultA],
[is(abcd.b), isResultB],
[is(abcd.c), isResultC],
[is(abcd.d), isResultD]
]);
for (let x of [abcd.a, abcd.b, abcd.c, abcd.d]) {
console.log(getCaseResult(x));
} // Logs 1, 2, 3, 4 respectively

Related

How to declare the return type of a generic arrow function to be an array and value at the same time in typescript?

Consider a function which takes an array, property name and an arrow function as arguments. What I want to achieve is, filter the array and overwrite the mentioned property's value with an array (output of the arrow function) based on the length check.
overwriteFieldWith<T1, T2 extends T1[keyof T1]>(
input: Array<T1>, property: keyof T1, onOverwrite: (i: T1) => T2
): T[] {
// filter the input array and replace the property for
// each item with the output of the supplied arrow function
return input.filter(i => {
const value = onOverwrite(i);
if(value.length) { // giving me error 'length does not exist on type T2'
i[property] = value;
console.log(i);
return true;
} else {
return false;
}
});
}
It gives me error when I try to do length check on it. The supplied arrow function is always going to return an array but how can I satisfy the compiler on this?
Edit:
Link to the ts playground.
Here's one approach:
overwriteFieldWith<T, K extends keyof T>(
input: Array<T>,
property: K,
onOverwrite: (i: T) => Extract<T[K], readonly any[]>
): T[] {
return input.filter(i => {
const value = onOverwrite(i);
if (value.length) {
i[property] = value;
return true;
} else {
return false;
}
});
}
I made the function generic in the type K of property, so that the type checker keeps track of which property we're talking about. That way we can require that onOverWrite() actually return something assignable to that property value instead of something else... we need to know this or else i[property] = value might be unsafe.
I also made the return type of onOverwrite() just Extract<T[K], readonly any[]> using the Extract<T, U> union filtering utility type instead of some generic U extends T[K].
(Note that the readonly any[] type is wider than just any[]; we don't care about modifying the array so we don't need to require mutable methods to exist).
This serves the purpose of convincing the compiler that value.length will exist (since the compiler accepts that Extract<T, U> is assignable to both T and U, so the compiler knows that value is an readonly any[]). It also serves the purpose of requiring that onOverwrite() return only those union members of the property type T[K] that are arrays. This works for your example use case at any rate.
Let's test it:
const filtered = a.overwriteFieldWith(elementTypes, 'elements', (i) =>
i?.elements?.filter(data => !data.hide) ?? []);
/* (method) A.overwriteFieldWith<ElementType, "elements">(input: ElementType[],
property: "elements", onOverwrite: (i: ElementType) => TElement[]
): ElementType[] */
Looks good. Note that the compiler inferred T as ElementType and K as "elements", and thus that the return type of onOverwrite() is TElment[] as demonstrated here:
type TK = ElementType['elements'];
//type TK = TElement[] | undefined
type XTK = Extract<ElementType['elements'], readonly any[]>
// type XTK = TElement[]
So an ElementType might have an undefined elements property, but onOverwrite cannot return undefined.
Playground link to code

How can I type the input/output types of successive transformer functions in TypeScript?

I have an array of transformer functions where the next one's input type should be the previous one's output type.
The first one's input type and last one's output type can be anything the user specifies.
// An array of these:
const transfomFn = (input) => {
// some transformation details...
return output:
}
How can I properly type these constrains as an array of transformers in TypeScript?
What is the type of this array that can enforce the right input/output types for each transformer in the array?
Thank you.
I believe this is not possible in typescript as the array constraints must be statically known.
I would achieve this with a utility class
type Transformer<I, O> = (input: I) => O;
class TransformerChain<
I,
O
> {
private transformers: ((x: unknown) => unknown)[] = [];
public addTransformer<U>(
transformer: Transformer<O, U>
): TransformerChain<I, U> {
const newTransformer = new TransformerChain<I, U>();
newTransformer.transformers = [...this.transformers, transformer as any];
return newTransformer;
}
public transform(data: I): O {
let cur: any = data;
for (const t of this.transformers) {
cur = t(cur);
}
return cur as O;
}
}
const t1 = new TransformerChain<number, string>();
const t2 = t1.addTransformer((x) => x.toString());
const t3 = t2.addTransformer((s) => [s] as const);
const result = t3.transform(3); // "result" is type readonly [string]

Typescript Typeguard check if array is of type

I want to write a typeguard to check if all children of array are of type T thus making it an Array where T is a generic type
TS Playground
// Assume arr of any type but Array
const arr: any[] = [
{
foo: "bleh1",
bar: 1
},
{
foo: "bleh2",
bar: 2
},
]
interface newType {
foo: string
bar: number
}
// Check that arr is an array of newType , ie arr: newType[]
const isArrayOf = <T,>(arr: any): arr is Array<T> => {
// TypeScript mastery needed here
return true
}
if(isArrayOf<newType>(arr)){
arr
}
The best thing you could do is this:
const arr: NewType[] = [
{
foo: "bleh1",
bar: 1
},
{
foo: "bleh2",
bar: 2
},
]
interface NewType {
foo: string
bar: number
}
type TypeOfArrayElements<T> = T extends Array<infer U> ? U : never;
type ArrayType = TypeOfArrayElements<typeof arr>;
TypeScript will never be able to guess that the array typed as any[] actually contains NewType elements. Like everything in TypeScript, type predicates are static and won't return a type dynamically based on what is passed as a parameter at runtime. But if you type it as NewType[], then you can extract the NewType type from it.
Edit 1:
This answer has two shortcomings (mentioned in comments)
The property needs not be own for it to exist on the object x = {a: 1}; y = Object.create(x); y.b = 2
The function requires you to manually enumerate all the keys of the object. Thus can introduce human error
I think the solution can still be used as workaround in specific circumstances
Original:
If an array a is of type newType[] then every element of a , consider x = a[0] will be of type newType. x is of type newType because x satisfies all properties and methods of type newType.
Thus if reversed , if x y z are of type newType and they are the only and all elements of array a, Thus every element of a are of type newType, which satisfies the condition for a to be of type newType[]
// Check if obj has all keys props[]
const hasAllProperties = <T,>(obj: any, props: (keyof T)[]): obj is T => {
return props.every((prop) => {
// console.log(prop)
return Object.prototype.hasOwnProperty.call(obj, prop)})
}
// Check that arr is an array of newType , ie arr: newType[]
const isArrayOf = <T,>(obj: any[], props: (keyof T)[]): obj is T[] => {
// Check if every elements have all keys in props
return obj.every((ele) => {
// console.log(ele)
return hasAllProperties<T>(ele,props)
}
)
}
if (isArrayOf<newType>(arr, ["foo", "bar"])) {
console.log("arr is of newType[]")
}
TS Playground

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'rowType' [duplicate]

When using Object.keys(obj), the return value is a string[], whereas I want a (keyof obj)[].
const v = {
a: 1,
b: 2
}
Object.keys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, []);
I have the error:
Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.
TypeScript 3.1 with strict: true. Playground: here, please check all checkboxes in Options to activate strict: true.
Object.keys returns a string[]. This is by design as described in this issue
This is intentional. Types in TS are open ended. So keysof will likely be less than all properties you would get at runtime.
There are several solution, the simplest one is to just use a type assertion:
const v = {
a: 1,
b: 2
};
var values = (Object.keys(v) as Array<keyof typeof v>).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
You can also create an alias for keys in Object that will return the type you want:
export const v = {
a: 1,
b: 2
};
declare global {
interface ObjectConstructor {
typedKeys<T>(obj: T): Array<keyof T>
}
}
Object.typedKeys = Object.keys as any
var values = Object.typedKeys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
Based on Titian Cernicova-Dragomir answer and comment
Use type assertion only if you know that your object doesn't have extra properties (such is the case for an object literal but not an object parameter).
Explicit assertion
Object.keys(obj) as Array<keyof typeof obj>
Hidden assertion
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
Use getKeys instead of Object.keys. getKeys is a ref to Object.keys, but the return is typed literally.
Discussions
One of TypeScript’s core principles is that type checking focuses on the shape that values have. (reference)
interface SimpleObject {
a: string
b: string
}
const x = {
a: "article",
b: "bridge",
c: "Camel"
}
x qualifies as a SimpleObject because it has it's shape. This means that when we see a SimpleObject, we know that it has properties a and b, but it might have additional properties as well.
const someFunction = (obj: SimpleObject) => {
Object.keys(obj).forEach((k)=>{
....
})
}
someFunction(x)
Let's see what would happen if by default we would type Object.keys as desired by the OP "literally":
We would get that typeof k is "a"|"b". When iterating the actual values would be a, b, c. Typescript protects us from such an error by typing k as a string.
Type assertion is exactly for such cases - when the programmer has additional knowledge. if you know that obj doesn't have extra properties you can use literal type assertion.
See https://github.com/microsoft/TypeScript/issues/20503.
declare const BetterObject: {
keys<T extends {}>(object: T): (keyof T)[]
}
const icons: IconName[] = BetterObject.keys(IconMap)
Will retain type of keys instead of string[]
1. npm install ts-extras (written by sindresorhus)
Use it:
import { objectKeys } from 'ts-extras'
objectKeys(yourObject)
That's it.
====
Here's another pkg I made before I knew about ts-extras:
npm install object-typed --save
import { ObjectTyped } from 'object-typed'
ObjectTyped.keys({ a: 'b' })
This will return an array of type ['a']
I completely disagree with Typescript's team's decision...
Following their logic, Object.values should always return any, as we could add more properties at run-time...
I think the proper way to go is to create interfaces with optional properties and set (or not) those properties as you go...
So I simply overwrote locally the ObjectConstructor interface, by adding a declaration file (aka: whatever.d.ts) to my project with the following content:
declare interface ObjectConstructor extends Omit<ObjectConstructor, 'keys' | 'entries'> {
/**
* Returns the names of the enumerable string properties and methods of an object.
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys<O extends any[]>(obj: O): Array<keyof O>;
keys<O extends Record<Readonly<string>, any>>(obj: O): Array<keyof O>;
keys(obj: object): string[];
/**
* Returns an array of key/values of the enumerable properties of an object
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T extends { [K: Readonly<string>]: any }>(obj: T): Array<[keyof T, T[keyof T]]>
entries<T extends object>(obj: { [s: string]: T } | ArrayLike<T>): [string, T[keyof T]][];
entries<T>(obj: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(obj: {}): [string, any][];
}
declare var Object: ObjectConstructor;
Note:
Object.keys/Object.entries of primitive types (object) will return never[] and [never, never][] instead of the normal string[] and [string, any][]. If anyone knows a solutions, please, feel free to tell me in the comments and I will edit my answer
const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };
// before
Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]
Object.entries(a) // [string, unknown][]
Object.entries(b) // [string, any][]
Object.entries(c) // [string, string|number][]
// after
Object.keys(a) // never[]
Object.keys(b) // never[]
Object.keys(c) // ('x'|'y')[]
Object.entries(a) // [never, never][]
Object.entries(b) // [never, never][]
Object.entries(c) // ['x'|'y', string|number][]
So, use this with caution...
You can use the Extract utility type to conform your param to only the keys of obj which are strings (thus, ignoring any numbers/symbols when you are coding).
const obj = {
a: 'hello',
b: 'world',
1: 123 // 100% valid
} // if this was the literal code, you should add ` as const` assertion here
// util
type StringKeys<objType extends {}> = Array<Extract<keyof objType, string>>
// typedObjKeys will be ['a', 'b', '1'] at runtime
// ...but it's type will be Array<'a' | 'b'>
const typedObjKeys = Object.keys(obj) as StringKeys<typeof obj>
typedObjKeys.forEach((key) => {
// key's type: 'a' | 'b'
// runtime: 'a', 'b', AND '1'
const value = obj[key]
// value will be typed as just `string` when it's really `string | number`
})
All that said, most developers would probably consider having numbers as keys a poor design decision/bug to be fixed.
Here is a pattern I use for copying objects in a typesafe way. It uses string narrowing so the compiler can infer the keys are actually types. This was demonstrated with a class, but would work with/between interfaces or anonymous types of the same shape.
It is a bit verbose, but arguably more straightforward than the accepted answer. If you have to do the copying operation in multiple places, it does save typing.
Note this will throw an error if the types don't match, which you'd want, but doesn't throw an error if there are missing fields in thingNum. So this is maybe a disadvantage over Object.keys.
class thing {
a: number = 1;
b: number = 2;
}
type thingNum = 'a' | 'b';
const thingNums: thingNum[] = ['a', 'b'];
const thing1: thing = new thing();
const thing2: thing = new thing();
...
thingNums.forEach((param) => {
thing2[param] = thing1[param];
});
playground link
Here's a more accurate utility function:
const keys = Object.keys as <T>(obj: T) =>
(keyof T extends infer U ? U extends string ? U : U extends number ? `${U}` : never : never)[];
Explanation: keyof T extends string | number | symbol, however Object.keys omits the symbol keys and returns number keys as strings. We can convert number keys to string with a template literal `${U}`.
Using this keys utility:
const o = {
x: 5,
4: 6,
[Symbol('y')]: 7,
};
for(const key of keys(o)) {
// key has type 'x' | '4'
}
As a possible solution, you can iterate using for..in over your object:
for (const key in myObject) {
console.log(myObject[key].abc); // works, but `key` is still just `string`
}
While this, as you said, would not work:
for (const key of Object.keys(myObject)) {
console.log(myObject[key].abc); // doesn't!
}

Rust type hint for static trait function

Consider this example:
trait MyTrait {
fn maybe_new() -> Option<Self>;
}
impl MyTrait for i32 {...}
fn hello() {
match MyTrait::maybe_new() {
Some(x) => ...,
None => ...,
}
}
This fails to compile because there is no way to infer the type of x. Is there some way to add a type annotation to make this work without having to break the maybe_new() into a let statement like this?:
let p:Option<i32> = MyTrait::maybe_new();
match p {
Some(x) => ...,
None => ...,
}
See How do I provide type annotations inline when calling a non-generic function?. In your case, it would look like this:
match <i32 as MyTrait>::maybe_new() {
Some(x) => ...,
None => ...,
}

Resources