繁体   English   中英

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

[英]How to type-annotate a TypeScript function that maps object properties?

背景

假设我要从此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;
}

这实际上似乎可行,但是有两个奇怪的地方:

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

助手类型:

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

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

// (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] }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM