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: "[email protected]",
}

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?

3 Answers 3

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>)
}
Sign up to request clarification or add additional context in comments.

1 Comment

This doesn't appear to work for me. Playground link: tsplay.dev/m02pDw
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]

3 Comments

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.
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
number | string, not input | string
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: "[email protected]",
}

const output: OutputType = mapObject<F_applyChanges, number | string>()
  (applyChanges, input);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.