如何对映射对象属性的TypeScript函数进行类型注释?

时间:2019-02-21 16:26:42

标签: typescript

背景

假设我要从此JSON加载对象:

{
  "dateStringA": "2019-01-02T03:04:05",
  "dateStringB": "2019-01-03T04:05:06",
  "nonDateString": "foobar",
  "someNumber": 123
}

因此,两个属性dateStringAdateStringB实际上应该是类型Date,但是由于JSON不知道类型Date,因此它是string并且需要进行转换。因此,一种选择可能是编写一个简单的映射函数,该函数在普通的旧JavaScript中转换如下所示的属性:

function mapProperties(obj, mapper, properties) {
  properties.forEach(function(property) {
    obj[property] = mapper(obj[property]);
  });
  return obj;
}
var usefulObject = mapProperties(
  jsonObject,
  function(val) {return new Date(val);},
  'dateStringA',
  'dateStringB'
);

问题

上面的方法很好,但是现在我想在TypeScript中做同样的事情,当然我想添加尽可能多的类型检查。所以在最好的情况下,我想得到以下结果:

// setup
const value = {dateStringA: '2019-01-02T03:04:05', dateStringB: '2019-01-03T04:05:06', nonDateString: '', someNumber: 123};
const result = mapProperties(value, (val: string): Date => new Date(val), 'dateStringA', 'dateStringB');

// --- TEST ---

// dateStringA & dateStringB should be dates now:
result.dateStringA.substr; // should throw compile error - substr does not exist on type Date
result.dateStringB.substr; // should throw compile error - substr does not exist on type Date
result.dateStringA.getDate; // should be OK
result.dateStringB.getDate; // should be OK

// nonDateString is still a string
result.nonDateString.substr; // should be OK
result.nonDateString.getDate; // should throw compile error - getDate does not exist on type string

// someNumber is still a number
result.someNumber.toFixed; // should be OK

// call not possible on properties that do not exist:
mapProperties(value, 'doesNotExist'); // should throw compile error

// call not possible on properties not of type string:
mapProperties(value, 'someNumber'); // should throw compile error

到目前为止我已经尝试过:

这是我一个人得到的最好成绩:

type PropertyNamesByType<O, T> = { [K in keyof O]: O[K] extends T ? K : never }[keyof O];
type OverwriteType<T, K extends keyof T, N> = Pick<T, Exclude<keyof T, K>> & Record<K, N>;

function mapProperties<
        WRAPPER_TYPE,
        WRAPPER_KEYS extends (keyof WRAPPER_TYPE & PropertyNamesByType<WRAPPER_TYPE, OLD_TYPE>),
        OLD_TYPE,
        NEW_TYPE
    >(obj: WRAPPER_TYPE,
      mapper: (value: OLD_TYPE) => NEW_TYPE,
      ...properties: WRAPPER_KEYS[]
    ): OverwriteType<WRAPPER_TYPE, WRAPPER_KEYS, NEW_TYPE> {

    const result: OverwriteType<WRAPPER_TYPE, WRAPPER_KEYS, NEW_TYPE> = <any>obj;
    properties.forEach(key => {
        (<any>result[key]) = mapper(<any>obj[key]);
    });
    return result;
}

这实际上似乎可行,但是有两个奇怪之处:<​​/ p>

  1. WRAPPER_KEYS extends (keyof WRAPPER_TYPE & PropertyNamesByType<WRAPPER_TYPE, OLD_TYPE>)行。我认为它应该仅适用于WRAPPER_KEYS extends PropertyNamesByType<WRAPPER_TYPE, OLD_TYPE>,而不能使用& keyof WRAPPER_TYPE,因为后者实际上不应添加任何其他信息(我很偶然地发现了这一点)。但是,如果我忽略了这一点,TypeScript的行为就像转换了所有字符串属性一样。那里发生了什么魔术?
  2. (<any>result[key]) = mapper(<any>obj[key]);行中,我需要那两个<any>广播。有什么办法可以摆脱这些?

2 个答案:

答案 0 :(得分:1)

助手类型:

type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
type Morphism<T = any, U = any> = (argument: T) => U;

示例实现:

const transform = <T, U extends Morphism<T[K]>, K extends keyof T>(source: T, mappingFn: U, ...properties: K[]) =>
  (Object.entries(source))
    .reduce(
      (accumulator, [key, value]) => {
        const newValue =
          properties.includes(key as K)
            ? mappingFn(value)
            : value

        return ({ ...accumulator, [key]: newValue })
      },
      {} as Overwrite<T, Record<K, ReturnType<U>>>
    );
  

备注:

     
      
  • U extends Morphism<T[K]>确保转换器仅接受properties(用T[K]表示)的值。
  •   
  • ReturnType需要TypeScript 2.8或更高版本
  •   

用法:

const source = {
  dateStringA: "2019-01-02T03:04:05",
  dateStringB: "2019-01-03T04:05:06",
  nonDateString: "foobar",
  someNumber: 123
}

const toDate = (date: string) => new Date(date);

console.log(
  transform(source, toDate, 'dateStringA', 'dateStringB')
)

答案 1 :(得分:0)

您可以映射属性是否出现在键列表中,然后使用转换后的类型还是原始类型:

// (just the type signature)
declare function mapProperties<Json, SourceType, TargetType, P extends keyof Json>(
    obj: Json,
    converter: (value: SourceType) => TargetType,
    ...keys: P[]): { [K in keyof Json]: K extends P ? TargetType : Json[K] }