2

For the following function which is similar to [].map but for objects

function mapObject(f, obj) {
  return Object.keys(obj).reduce((ret, key) => {
    ret[key] = f(obj[key])
    return ret
  }, {})
}

is there a way to type it so that the following works?

interface InputType {
  numberValue: number
  stringValue: string
}

interface OutputType {
  numberValue: string
  stringValue: number
}

const input: InputType = {
  numberValue: 5,
  stringValue: "bob@gmail.com",
}

function applyChanges(input: number): string
function applyChanges(input: string): number
function applyChanges(input: number | string): number | string {
  return typeof input === "number" ? input.toString() : input.length
}

const output: OutputType = mapObject(applyChanges, input) // <-- How to get the correct 'OutputType'

This works, but is very specific to the applyChanges function

type MapObject<T> = {
  [K in keyof T]: T[K] extends number
    ? string
    : T[K] extends string ? number : never
}

function mapObject<F extends FunctionType, T>(f: F, obj: T): MapObject<T>

Is there a more general solution?

Lionel Tay
  • 1,274
  • 2
  • 16
  • 28

3 Answers3

1

There is a signature in the typescript 2.1 release notes.

Combined with your code I end up with:

function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U> {
    return Object.keys(obj).reduce((ret, key) => {
        const k = key as K;
        ret[k] = f(obj[k]);
        return ret
    }, {} as Record<K, U>)
}
mazatwork
  • 1,275
  • 1
  • 13
  • 20
0

Yes, you can use a lambda type to describe the type of the input and output of f, and then add a constraint that the input type of f, here called A, must be part of the type of the values of the type of obj, somewhat obscurely referred to as O[keyof O]

function mapObject<A extends O[keyof O], B, O>(f: (a: A) => B, obj: O) {
  return Object.keys(obj).reduce((ret, key) => {
    ret[key] = f(obj[key])
    return ret
  }, {})
}

As suggested here, you may introduce a type alias to improve readability when using keyof:

type valueof<T> = T[keyof T]
Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
  • Thanks for the response. With the `A extends O[keyof O]` we ensure that the argument to the function is assignable to a value of the object, but actually we need the restriction that any value of the object is assignable to the argument of the function as the function will be called with every value. For example, with these types, if the function could only take strings it would typecheck, however it would not be able to be called with the `numberValue`. Also I was wondering if there is a way to type the output of `mapObject` so that the new object type is correct. – Lionel Tay Oct 11 '18 at 13:01
  • You're right, it somehow typechecks even though you substitute applyChanges for a function whose input is only number or only string, even though the type of O[keyof O] for the given input is equivalent to input | string – Jonas Høgh Oct 11 '18 at 13:22
  • number | string, not input | string – Jonas Høgh Oct 11 '18 at 13:28
0

You would need higher-kinded types to properly describe the type transformation performed by mapObject in terms of that performed by f. If you use my favorite mini-library for faking higher-kinded types, you can set things up like this:

// Matt's mini "type functions" library

const INVARIANT_MARKER = Symbol();
type Invariant<T> = { [INVARIANT_MARKER](t: T): T };

interface TypeFuncs<C, X> {}

const FUN_MARKER = Symbol();
type Fun<K extends keyof TypeFuncs<{}, {}>, C> = Invariant<[typeof FUN_MARKER, K, C]>;

const BAD_APP_MARKER = Symbol();
type BadApp<F, X> = Invariant<[typeof BAD_APP_MARKER, F, X]>;
type App<F, X> = [F] extends [Fun<infer K, infer C>] ? TypeFuncs<C, X>[K] : BadApp<F, X>;

// Scenario

// https://github.com/Microsoft/TypeScript/issues/26242 will make this better.    
function mapObject<F, B>() {
  return function <O extends { [P in keyof O]: B }>
    (f: <X extends B>(arg: X) => App<F, X>, obj: O): {[P in keyof O]: App<F, O[P]>} {
    return Object.keys(obj).reduce((ret, key) => {
      const key2 = <keyof O>key;
      ret[key2] = f(obj[key2])
      return ret
    }, <{[P in keyof O]: App<F, O[P]>}>{})
  };
}

const F_applyChanges = Symbol();
type F_applyChanges = Fun<typeof F_applyChanges, never>;
interface TypeFuncs<C, X> { 
  [F_applyChanges]: X extends number ? string : X extends string ? number : never;
}

// Take advantage of the lax checking of overload signatures.  With
// https://github.com/Microsoft/TypeScript/issues/24085, we may be able
// to type check the body of applyChanges based on the first signature.
function applyChanges<X extends number | string>(input: X): App<F_applyChanges, X>
function applyChanges(input: number | string): number | string {
  return typeof input === "number" ? input.toString() : input.length;
}

interface InputType {
  numberValue: number
  stringValue: string
}

interface OutputType {
  numberValue: string
  stringValue: number
}

const input: InputType = {
  numberValue: 5,
  stringValue: "bob@gmail.com",
}

const output: OutputType = mapObject<F_applyChanges, number | string>()
  (applyChanges, input);
Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75