Typescript specific string value in the array - reactjs

Is Tuple in typescript to achieve this to check the value sequence in the array? I'd like to check all the value inside the array must follow the sequence.
index: 0 is "hello", index: 1 is "world" and index: 2 is "morning"
As example:
const correctSequence = ['hello', 'world', 'morning']
const inCorrectSequence = ['world', 'hello', 'morning']
const Component = () => {
return (
<>
<Child names={correctSequence} />
<Child names={inCorrectSequence} />
</>
)
}
type Names = 'hello' | 'world' | 'morning'
interface ChildProps {
names: Names[]; // ---> not complaint in incorrect array order
}
const Child = ({names}: ChildProps) => {}

The pipe( | ) operator works as a or operator in typescript so what is happening in your ChildProps is that the typescript understands that the names field should be an array and the values of that array should be either 'hello', 'world' or 'morning. The sequence won't matter.
If you want to force the sequence then directly declare the values in the type as below.
type Names = ["hello", "world", "morning"]
interface ChildProps = {
names: Names
}
This will force the Child component to only accept ["hello", "world", "morning"] values with the specified sequence.

Related

Map Typescript generic array

Suppose I have an object with a static set of keys, and then a type specifying a subset of those keys as a static array:
const myBigStaticObject = {
key1: () => 'foo',
key2: () => 'bar',
// ...
keyN: () => 'fooN',
}
type BigObject = typeof myBigStaticObject
type Keys = ['key2', 'keyN']
Is there a nice way of then programmatically creating a mapped type which takes Keys as its generic argument, i.e. does the following:
type ObjectValueFromKeys<K extends (keyof BigObject)[]> = // ???
// such that:
ObjectValueFromKeys<Keys> = [() => 'bar', () => 'fooN']
The equivalent for objects here would be:
const objectKeys = {
a1: 'key2';
a2: 'keyN';
}
type ObjectValueFromObjectKeys<M extends Record<string, keyof BigObject>> = {
[K in keyof M]: BigObject[M[K]]
}
// then
type ObjectValueFromObjectKeys<typeof objectKeys> = {
a1: () => 'bar';
a2: () => 'fooN';
}
You may not know this but you can actually use a mapped type with arrays and tuples while still retaining the array/tuple type in the output!
That means you can use something like this, where it checks if the value is a key of the big object first:
type ObjectValueFromKeys<K extends (keyof BigObject)[]> = {
[P in keyof K]: K[P] extends keyof BigObject ? BigObject[K[P]] : K[P];
};
And it works like a charm:
type T1 = ObjectValueFromKeys<["key2", "keyN"]>;
// ^? [() => "bar", () => "fooN"]
Don't believe me? See for yourself.
You will probably notice that there is as const used in the big object:
key1: () => 'foo' as const,
This is because, for some reason, TypeScript can't deduce that this function can only return "foo". And so I have used as const to make it return the literal "foo" for our case so we can see if the solution works.

How to define generic types for each object in an array?

I'm building a form in react. The form has inputs with different data types. It can be string or number, or array of types, basically unknown. The user has to define it.
With a single Input it's straightforward: see playground.
import React, { useState } from 'react';
type Props<T> = {
initValue: T,
process: (value: T) => T
}
export const Input = <T,>({
initValue,
process,
}: Props<T>): React.ReactElement => {
const [value, setValue] = useState<T>(initValue);
return (
<input
value={value as unknown as string}
onChange={({ target: { value: dirtyValue } }) => {
const processedValue = process(dirtyValue as unknown as T);
setValue(processedValue);
}}
/>
);
};
() => (
// fails because value is number, and indexOf doesn't exist on number
<Input initValue={4} process={value => `${value.indexOf('4')}`} />
);
() => (
// fails because the return type of process is number, not string
<Input initValue={'4'} process={value => value.indexOf('4')} />
);
() => (
// passes because value is string, indexOf exists on string, and process returns string
<Input initValue={'4'} process={value => `${value.indexOf('4')}`} />
);
The above code doesn't really make sense, it just explains what I would like to reach. I want to raise this behaviour to the next level, so the inputs are controlled centrally by a form component, and the inputs are passed to its props like:
<Form
inputs={[
{
id: 'string',
initValue: '4', // `initValue` type is string, so `process` will get string as well, and must return string
process: value => `${value.indexOf('4')}`,
},
{
id: 'number',
initValue: 4, // `initValue` type is number, so `process` will get number as well, and must return number
process: value => value + 2,
},
{
id: 'array',
initValue: [0, 1, 2], // `initValue` type is array of numbers, so `process` will get array of numbers as well, and must return array of numbers
process: values => values.filter(Boolean),
},
]}
onSubmit={({ string, number, array }) => {
// inside the component the array of objects are reduced to a single object, where key is `id`,
// value is value corresponding to it, matching the type defined in `initValue`
console.log(
typeof string === 'string' &&
typeof number === 'number' &&
Array.isArray(array)
); //ideally logs true
}}
/>
I was playing around, and trying out different things to reach what I want, but the issue with my try is I can only define one type generic for an array. So wrapping the Input component's typing logic with an array, I have to specify the type generic. So if I try to have:
type InputProps<T> = {
id: string;
initValue: T;
process: (value: T) => T;
};
type InputList = InputProps[]; // Generic type 'InputProps' requires 1 type argument(s)
type Props<Inputs extends InputList> = {
inputs: Inputs;
onSubmit: (values: InputList) => unknown;
};
Basically I could have an array of input props, but with only one type for all. I want each object to have its own type in the array, so I can have the same behaviour when defining the Form component's inputs prop, as the Input component's props.
In the Form, instead of me manually specifying the Inputs, I want to have this component that collects, and controls them in one place. Obviously inside Form component it will be a black box, but I want to be able to match the types from the outside when I use it. So process function (just as in the example of Input above) knows that it will receive its value parameter with the same type as initValue, but also I want to construct an object with the keys of each object's id in the inputs array, and the value must match the type of the object's initValue.
Is this possible based on the above?
EDIT
I was able to produce something that seemed working in the beginning, but then realised some weird things are going on see playground.
It looks like this:
import React from 'react';
type InputProps<T extends InputList<T>, K extends keyof T> = {
initValue: T[K]['initValue'];
process: (value: T[K]['initValue']) => T[K]['initValue'];
};
type InputList<T extends InputList<T>> = {
[K in keyof T]: InputProps<T, K>
};
type Values<T extends InputList<T>> = {
[K in keyof T]: T[K]['initValue']
}
type Props<T extends InputList<T>> = {
inputs: T;
onSubmit: (values: Values<T>) => unknown;
};
export const Form = <T extends InputList<T>,>({
inputs, onSubmit
}: Props<T>) => (
<div></div>
);
() => (
<Form
inputs={{
string: {
initValue: '4', // `initValue` type is string, so `process` will get string as well, and must return string
process: value => `${value.indexOf('4')}`,
},
number: {
initValue: 4, // `initValue` type is number, so `process` will get number as well, and must return number
process: value => value + 2,
},
array: {
initValue: [0, 1, 2], // `initValue` type is array of numbers, so `process` will get array of numbers as well, and must return array of numbers
process: values => values.filter(Boolean),
},
}}
// onSubmit={({ string, number, array }) => {
// // inside the component the array of objects are reduced to a single object, where key is `id`,
// // value is value corresponding to it, matching the type defined in `initValue`
// console.log(
// typeof string === 'string' &&
// typeof number === 'number' &&
// Array.isArray(array)
// ); //ideally logs true
// }}
/>
);
If you type the process function, it gives a peek about what the function's type should be, but once written, it shows the value as unknown. Also, once onSubmit is defined, everything is messed up, and it's not even sure about the key types, so something's still wrong.

TS React prop as a dynamic key in array of objects

I'm passing a prop to a react component to act as dynamic key (property accessor) for the array of objects (passed as the other prop).
Something like this:
const MyComp = ({accessor, data}) => (<div>{data.map(val => <p>{val[accessor]}</p>)}</div>)
I'm trying to create a type definition for this component, that will check if:
accessor is a string
value of data[i][accessor] exists and is a string
const data = [{foo: 'bar'}, {foo: 'baz'}]
...
<MyComp accessor="foo" data={data} /> // that compiles as both conditions are fulfilled
<MyComp accessor="wrong" data={data} /> // ts should give a warning that `accessor` is not assignable
I kinda did it for a simple function:
const data = [{ foo: "bar" }, { foo: "baz" }];
type Image = Record<string, any>
function getAProp<T extends Image, K extends keyof T>(
arr: T[],
key: K
): string[] {
return arr.map((v) => `${v[key]}, `);
}
getAProp(data, "foo") // returns a nice array of strings
getAProp(data, "wrong") // ts screams: Argument of type '"wrong"' is not assignable to parameter of type '"foo"'
But just can't wrap my head around it how to recreate that as a functional component.
Here's a sandbox with the code.
https://codesandbox.io/s/ts-dynamic-property-prop-problem-6vz86
Thanks a lot!

How to create a custom equality function with reselect and Typescript?

A standard reselect selector invalidates its memoized value and recomputes it if the input selectors fail a strict equality check:
export const selectEmailsFromComments = createSelector(
selectComments, // returns an array of comments
comments => commentsToEmails(comments)
)
Since the comments are an array and not a primitive value, and redux reducers tend to create a new piece of state to avoid side effects, the above seems to never actually memoize because the comments array returned by selectComments will always have a different reference.
To resolve this, one can create a custom selector creator, e.g. to introduce shallow equality checks:
const createShallowEqualSelector = createSelectorCreator(
defaultMemoize,
shallowEqual
)
export const selectEmailsFromComments = createShallowEqualSelector(
selectComments, // returns an array of comments
comments => commentsToEmails(comments)
)
This works if the comments indeed are simple objects and we want to recompute the emails whenever any of the comments' props changed.
But what if we only want to recompute the emails if e.g. the number of comments changed? How could we implement a custom equality check? I would expect the following to work:
type ComparisonFunction<B extends object = {}> = (prev: B, next: B, index: number) => boolean
const createCustomEqualSelector = <B extends object = {}>(
equalFn: ComparisonFunction<B>
) => createSelectorCreator<ComparisonFunction<B>>(defaultMemoize, equalFn)
const commentsEqualFn = (a: IComment[], b: IComment[], index: number) =>
a.length === b.length
export const selectEmailsFromComments = createCustomEqualSelector(
commentsEqualFn
)(
selectComments, // returns an array of comments
comments => commentsToEmails(comments)
)
However this returns the following Typescript error for defaultMemoize:
(alias) function defaultMemoize<F extends Function>(func: F, equalityCheck?: (<T>(a: T, b: T, index: number) => boolean) | undefined): F
import defaultMemoize
Argument of type '<F extends Function>(func: F, equalityCheck?: (<T>(a: T, b: T, index: number) => boolean) | undefined) => F' is not assignable to parameter of type '<F extends Function>(func: F, option1: ComparisonFunction<B>) => F'.
Types of parameters 'equalityCheck' and 'option1' are incompatible.
Type 'ComparisonFunction<B>' is not assignable to type '<T>(a: T, b: T, index: number) => boolean'.
Types of parameters 'prev' and 'a' are incompatible.
Type 'T' is not assignable to type 'B'.ts(2345)
How would I resolve this type error for a custom reselect createSelector equality function?
The following worked for me but I'm not sure if it's the best way.
import {
createSelectorCreator,
defaultMemoize,
} from 'reselect';
type IComment = { id: number };
type State = { comments: IComment[] };
type CompareFn = <T>(a: T, b: T, index: number) => boolean;
const createCustomEqualSelector = (equalFn: CompareFn) =>
createSelectorCreator(defaultMemoize, equalFn);
const selectComments = (state: State) => state.comments;
const commentsEqualFn: CompareFn = (a, b, index) =>
//need to cast it or get an error:
// Property 'length' does not exist on type 'T'
((a as unknown) as IComment[]).length ===
((b as unknown) as IComment[]).length;
export const selectEmailsFromComments = createCustomEqualSelector(
commentsEqualFn
)(
selectComments, // returns an array of comments
(comments) => {
console.log('calculating comments:', comments);
return comments;
}
);
selectEmailsFromComments({ comments: [{ id: 1 }] })
//this will log previous value because array length didn't change
console.log('memoized', selectEmailsFromComments({ comments: [{ id: 2 }] }))

Typescript generics for react component taking array of records and array of keys

I'm trying to create a simple typed react component for rendering a table from an array of objects. The data input is in the format of:
// array of records containing data to render in the table
data = [
{
one: 1,
two: 2,
three: 3,
},
{
one: 11,
two: 22,
three: 33,
}
]
// subset of keys from the data to render in the table
labels = [
'one',
'three',
]
The component is as follows:
function Table<T extends Record<string, string | number>, K extends Extract<keyof T, string>>({
labels,
data
}: {
labels: K[];
data: T[];
}) {
return (
<table className="table">
{/* ... */}
</table>
);
}
Testing it, it seems to work only when the labels is created in the prop and not before:
// Works
<Table labels={['one']} data={data} />
// Does not work
const labels = ['one']
<Table labels={labels} data={data} />
// Type 'string[]' is not assignable to type '("one" | "two" | "three")'
Does anyone know how to fix the typing so that the second method works and I don't have to create the labels array inline?
Typescript will not infer string literal types unless it has to. If you write const labels = ['one']; typescript will widen the type of labels to string[].
You can get araound this either by using as const in 3.4 (unreleased yet) but using as const which will make ts infer a read-only tuple for labels
const labels = ['one'] as const; // const labels: readonly ["one"]
Before 3.4 we can either use an explicit type:
const labels: ["one"] = ['one'];
Or use an helper function to hint to the compiler to infer string literal and tuples:
function stringTuple<T extends string[]>(...a: T){
return a;
}
const labels = stringTuple('one'); // const labels: ["one"]
By defining Data first
interface Data {
one: number,
two: number,
three: number,
}
const labels: keyof Data[] = ['one']
By reading the type of Data
const labels: keyof Unpacked<typeof data>[] = ['one']
type Unpacked<T> =
T extends Array<infer U>
? U
: T;
By using a type assertion
<Table labels={labels as (keyof Data)[]} data={data} />

Resources