【问题标题】:Typescript: Generic object mapping functionTypescript:通用对象映射函数
【发布时间】:2020-06-09 23:36:49
【问题描述】:

对于以下类似于[].map但针对对象的函数

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

有没有办法输入它以便以下工作?

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'

这可行,但非常特定于 applyChanges 函数

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>

有没有更通用的解决方案?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    typescript 2.1 release notes中有签名。

    结合你的代码,我最终得到:

    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>)
    }
    

    【讨论】:

    【解决方案2】:

    可以,可以用一个lambda类型来描述f的输入和输出的类型,然后加一个约束,f的输入类型,这里叫A,必须是obj 类型的值的类型,有些晦涩地称为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
      }, {})
    }
    

    按照here的建议,您可以在使用 keyof 时引入类型别名以提高可读性:

    type valueof<T> = T[keyof T]
    

    【讨论】:

    • 感谢您的回复。使用A extends O[keyof O],我们确保函数的参数可以分配给对象的值,但实际上我们需要限制对象的任何值都可以分配给函数的参数,因为函数将被调用价值。例如,对于这些类型,如果函数只能接受字符串,它会进行类型检查,但是不能用numberValue 调用它。另外我想知道是否有办法输入mapObject 的输出,以便新的对象类型正确。
    • 你是对的,即使你将 applyChanges 替换为输入仅为数字或字符串的函数,它也会以某种方式进行类型检查,即使给定输入的 O[keyof O] 的类型等同于输入 |字符串
    • 编号 |字符串,不是输入 |字符串
    【解决方案3】:

    您将需要higher-kinded types 来正确描述mapObject 执行的类型转换,该转换由f 执行。如果你使用我最喜欢的迷你库来伪造更高种类的类型,你可以这样设置:

    // 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);
    

    【讨论】:

      猜你喜欢
      • 2023-01-18
      • 2022-01-23
      • 2020-12-06
      • 2014-11-07
      • 1970-01-01
      • 2019-02-16
      • 1970-01-01
      • 1970-01-01
      • 2011-02-24
      相关资源
      最近更新 更多