Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: How to strongly-type a function that transforms a map of functions into similar functions with a different type param?

I'm trying to strongly-type a globalizeSelectors function that would transform a map of redux selector functions such that they will accept a GlobalState type instead of their StateSlice type based on the key of their StateSlice (where StateSlice means it's a value of one of the GlobalState object's properties).

The tricky part is that the return types of the selectors can all be different, and I don't quite know how to type that variation (or if it's even possible). Based on the typescript docs, I'm guessing this might involve some clever use of the infer operator, but my typescript-fu isn't quite at that level yet.

Here's what I've got so far: (BTW, for you reduxy types, nevermind the fact that these selectors don't handle props or additional args -- I've removed that to simplify this a bit)

import { mapValues } from 'lodash'

// my (fake) redux state types
type SliceAState = { name: string }
type SliceBState = { isWhatever: boolean }

type GlobalState = {
  a: SliceAState;
  b: SliceBState;
}

type StateKey = keyof GlobalState

type Selector<TState, TResult> = (state: TState) => TResult

type StateSlice<TKey extends StateKey> = GlobalState[TKey]

type GlobalizedSelector<TResult> = Selector<GlobalState, TResult>

const globalizeSelector = <TKey extends StateKey, Result>(
  sliceKey: TKey,
  sliceSelector: Selector<StateSlice<TKey>, Result>
): GlobalizedSelector<Result> => state => sliceSelector(state[sliceKey])

// an example of a map of selectors as they might be exported from their source file
const sliceASelectors = {
  getName: (state: SliceAState): string => state.name,
  getNameLength: (state: SliceAState): number => state.name.length
}

// fake global state
const globalState: GlobalState = {
  a: { name: 'My Name' },
  b: { isWhatever: true }
}

// so this works...
const globalizedGetName = globalizeSelector('a', sliceASelectors.getName)
const globalizedNameResult: string = globalizedGetName(globalState)

const globalizedGetNameLength = globalizeSelector(
  'a',
  sliceASelectors.getNameLength
)
const globalizedNameLengthResult: number = globalizedGetNameLength(globalState)

/* but when I try to transform the map to globalize all its selectors, 
   I get type errors (although the implementation works as untyped
   javascript):
*/
type SliceSelector<TKey extends StateKey, T> = T extends Selector<
  StateSlice<TKey>,
  infer R
>
  ? Selector<StateSlice<TKey>, R>
  : never

const globalizeSelectors = <TKey extends StateKey, T>(
  sliceKey: TKey,
  sliceSelectors: {
    [key: string]: SliceSelector<TKey, T>;
  }
) => mapValues(sliceSelectors, s => globalizeSelector(sliceKey, s))

const globalized = globalizeSelectors('a', sliceASelectors)
/*_________________________________________^ TS Error:
Argument of type '{ getName: (state: SliceAState) => string; getNameLength: (state: SliceAState) => number; }' is not assignable to parameter of type '{ [key: string]: never; }'.
  Property 'getName' is incompatible with index signature.
    Type '(state: SliceAState) => string' is not assignable to type 'never'. [2345]
*/
const globalizedGetName2: string = globalized.getName(globalState)
like image 389
James Nail Avatar asked Nov 29 '18 06:11

James Nail


People also ask

What are the two parts of a function in typescript?

Introduction to TypeScript function types A function type has two parts: parameters and return type. When declaring a function type, you need to specify both parts with the following syntax: (parameter: type, parameter:type,...) => type

What is a weak map in typescript?

Weak Map in TypeScript The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced. The keys must be objects and the values can be arbitrary values. Keys of WeakMaps are of the type Object only.

What is array map in typescript?

The Array.map() is an inbuilt TypeScript function that is used to create a new array with the results of calling a provided function on every element in this array. Syntax: array.map(callback[, thisObject])

Can typescript infer the type of a type argument?

Specifying Type Arguments TypeScript can usually infer the intended type arguments in a generic call, but not always. For example, let’s say you wrote a function to combine two arrays: function combine < Type > (arr1: Type [], arr2: Type []): Type [] {


1 Answers

In globalizeSelectors the type sliceSelectors is {[key: string]: SliceSelector<TKey, T> }. But who should T be in this case? In your simple version T is going to be the return type of that particular slice selector, but when you map multiple T can't be all the return types.

The solution I would use is to use T to rerpesent the whole type of sliceSelectors with th restriction that all memebers must be of type SliceSelector<TKey, any>. The any there just represents we don't care what the return type of the slice selectors are.

Even though we don't care what the return type of each slice selector is, T will capture the type acturately (ie the return types of each function in the object will not be any but the actual type). We can then use T to create a mapped type that globalizez each function in the object.

import { mapValues } from 'lodash'

// my (fake) redux state types
type SliceAState = { name: string }
type SliceBState = { isWhatever: boolean }

type GlobalState = {
  a: SliceAState;
  b: SliceBState;
}

type StateKey = keyof GlobalState


type GlobalizedSelector<TResult> = Selector<GlobalState, TResult>

const globalizeSelector = <TKey extends StateKey, Result>(
  sliceKey: TKey,
  sliceSelector: Selector<StateSlice<TKey>, Result>
): GlobalizedSelector<Result> => state => sliceSelector(state[sliceKey])

// an example of a map of selectors as they might be exported from their source file
const sliceASelectors = {
  getName: (state: SliceAState): string => state.name,
  getNameLength: (state: SliceAState): number => state.name.length
}

// fake global state
const globalState: GlobalState = {
  a: { name: 'My Name' },
  b: { isWhatever: true }
}

type Selector<TState, TResult> = (state: TState) => TResult

type StateSlice<TKey extends StateKey> = GlobalState[TKey]

// Simplified selctor, not sure what the conditional type here was trying to achive
type SliceSelector<TKey extends StateKey, TResult> = Selector<StateSlice<TKey>, TResult>

const globalizeSelectors = <TKey extends StateKey, T extends {
    [P in keyof T]: SliceSelector<TKey, any>
  }>(
  sliceKey: TKey,
  sliceSelectors: T
) : { [P in keyof T]: GlobalizedSelector<ReturnType<T[P]>> } => mapValues(sliceSelectors, s => globalizeSelector(sliceKey, s as any)) as any // Not sure about mapValues 

const globalized = globalizeSelectors('a', sliceASelectors)

const globalizedGetName2: string = globalized.getName(globalState)

The only small issue is that mapValues needs some type assertions to work, I don't think mapValues is equiped to deal with these types.

like image 124
Titian Cernicova-Dragomir Avatar answered Sep 29 '22 11:09

Titian Cernicova-Dragomir