将函数转换为使用打字稿类型

时间:2019-05-01 17:27:41

标签: javascript typescript

我具有以下功能:

const selector = (definitions, query = {}) => {
  const select = {};

  Object.keys(definitions).forEach((key) => {
    if (typeof query[key] !== 'undefined') {
      if (definitions[key].validation(query[key])) {
        if (typeof definitions[key].convert !== 'undefined') {
          select[key] = definitions[key].convert(query[key], key);
        } else {
          select[key] = query[key];
        }
      } else if (typeof definitions[key].default !== 'undefined') {
        select[key] = definitions[key].default;
      }
    } else if (typeof definitions[key].default !== 'undefined') {
      select[key] = definitions[key].default;
    }
  });

  return select;
};

此打字稿报告我需要输入正确的definitionsquery,但我真的不知道怎么写。

错误消息。 Element implicitly has an 'any' type because type '{}' has no index signature.

我该怎么办?


用法:

const config = {
  name: {
    validation: val => val.length >= 3
  },
  page: {
    validation: val => Number.isInteger(parseInt(val, 10)),
    default: 1,
  },
  limit: {
    default: 10,
  }
}

const myQuery = {
  name: 'da',
  page: 'no-unber',
  another: '1',
}
select(config, myQuery)

测试

const expected = JSON.stringify({
  page: 1,
  limit: 10,
})
const output = JSON.stringify(selector(config, myQuery))

if (expected !== output) {
  throw new Error(`The output is wrong.\nExpected: ${expected}\nOutput: ${output}`)
}

使用案例:https://jsbin.com/gatojir/edit?js,console

注意:validation是必需的,但是convertdefault是可选参数。

2 个答案:

答案 0 :(得分:1)

索引签名是当您指定对象的各种键将对应于某种类型时,即使您事先不确切知道它们将是哪些键(或不想枚举它们)也是如此。

例如,您的definitions参数可能定义如下:

interface definition {
  validation?: (arg: any) => boolean;
  convert?: (arg0: any, arg1: any) => any;
  default?: number;
}

interface definitionsCollection {
  [key: string]: definition; // <--- this is an index signature
}

const selector = (definitions: definitionsCollection, query = {}) => {
  // ... etc
}

请注意,您的代码还有另一个问题:您在调用definitions[key].validation(query[key])时未确保先定义definitions[key].validation。您将其描述为必需的,但是您自己的示例表明它可能不存在,因为limit没有验证功能。如果需要支持不存在验证的情况,则需要在以下位置添加该检查:

  Object.keys(definitions).forEach((key) => {
    const def = definitions[key];
    if (typeof query[key] !== 'undefined') {
      if (typeof def.validation !== 'undefined' && def.validation(query[key])) {
        if (typeof def.convert !== 'undefined') {
          select[key] = def.convert(query[key], key);
        } else {
          select[key] = query[key];
        }
      } else if (typeof def.default !== 'undefined') {
        select[key] = def.default;
      }
    } else if (typeof def.default !== 'undefined') {
      select[key] = def.default;
    }
  });

答案 1 :(得分:0)

nicholasdefinition参数提供了一个很好的解决方案。我们可以对其进行增强,使其更通用,更明确,并为query参数指定类型:

type QueryValue = any;
type Query = { [key: string]: QueryValue };

type Definition<T1, T2=T1> = {
  validation?: (value: QueryValue) => boolean;
  convert   ?: (value: QueryValue, key: string) => T1;
  default   ?: T2;
};

注意:

  • 类型别名QueryValue有助于阐明querydefinition的值在哪里使用。
  • definitions类型将不具有类型definitionsCollection suggested by nicholas,而将具有通用类型D。调用selector()函数时,它不那么用户友好,但这是推断每个键的值类型的唯一方法。

对于返回类型(当前为{}!),这确实是一个挑战。我们只需要在definitionsquery中选择一些键,这些键中的一些是可选的,取决于定义的validation()方法的结果。

基本思想是创建一个Partial之类的mapped type,并结合一些带有类型推断的conditional types criteria

要选择键,我们不能使用never类型的值:类型{ a: never }不会减少为{}。我们必须过滤“内联”键,用过滤后的键重新组成对象并提取这些键!我从distributive conditional types中发现的FunctionPropertyNames<T>中汲取了这个想法。

要区分可选键,我们别无选择,只能将定义拆分为2,然后将它们重新组合为交集类型:type T = { K: V } & { K?: V }。实际上,{ K: V|undefined }不等于{ K?: V }

使用一些帮助程序类型并处理不同的情况,结果是:

type WithValidation = { validation: (value: QueryValue) => boolean; }
type WithConvert<T> = { convert   : (value: QueryValue, key: string) => T; }
type WithDefault<T> = { default   : T; }

type SelectorResult<D> = {
  [P in {
    [K in keyof D]:
      D[K] extends WithDefault<any> ? K : never
  }[keyof D]]:
    D[P] extends WithDefault<infer T> ? T : never;
} & {
  [P in {
    [K in keyof D]:
      D[K] extends WithDefault<any> ? never :
      D[K] extends WithConvert<any> ?
      D[K] extends WithValidation   ? K : never :
      D[K] extends Definition<any>  ? K : never
  }[keyof D]]?:
    D[P] extends WithValidation & WithConvert<infer T> ? T :
    D[P] extends Definition<any> ? unknown : never;
};

我们可以使用一些可以在编辑器中评估的“测试类型”来验证SelectorResult类型。结果很好,但是如果合并了交叉点类型,那就更好了:

type Test1 = SelectorResult<{ a: 'invalid' }>;
// Not a `Definition<T>`
// => Expected `{}`
// >> Received `{} & {}` : OK

type Test2 = SelectorResult<{ a: any }>;
// ~`Definition<unknown>`
// => Expected `{ a?: unknown }`
// >> Received `{ a: {} } & { a?: unknown }` : ~OK

type Test3 = SelectorResult<{ a: WithConvert<any> }>;
// `convert` alone is ignored.
// => Expected `{}`
// >> Received `{} & {}` : OK

type Test4 = SelectorResult<{ a: WithDefault<number> }>;
// `default` ensures the key is kept and provides the value type: `number`.
// => Expected `{ a: number }`
// >> Received `{ a: number } & {}` : OK

type Test5 = SelectorResult<{ a: WithDefault<number> & WithConvert<any> }>;
// Idem Test 4: `convert()` still ignored, only `default` matters.
// => Expected `{ a: number }`
// >> Received `{ a: number } & {}` : OK

type Test6 = SelectorResult<{ a: WithValidation }>;
// `validation()` alone makes the key optional and the value type `unknown`.
// => Expected `{ a?: unknown }`
// >> Received `{} & { a?: unknown }` : OK

type Test7 = SelectorResult<{ a: WithValidation & WithConvert<string> }>;
// Idem Test6 + `convert()` providing the value type: `string`
// => Expected `{ a?: string }`
// >> Received `{} & { a?: string | undefined }` : OK

type Test8 = SelectorResult<{ a: WithDefault<number>; b: 'invalid'; c: WithValidation }>;;
// Combining several keys from different cases: a (Test4), b (Test1), c (Test6)
// => Expected `{ a: number; c?: unknown }`
// >> Received `{ a: number } & { c?: unknown }` : OK

可以在下面的代码中找到类型的用法,只需进行一些重构即可使用更多功能:

const keySelector = <D, Q>(definitions: D, query: Q, key: string) => {
  const val = query[key as keyof Q];
  const def = definitions[key as keyof D] as Definition<D[keyof D]>;

  if (typeof val !== 'undefined' &&
      typeof def.validation !== 'undefined' &&
      def.validation(val)) {
    return typeof def.convert !== 'undefined'
        ? { [key]: def.convert(val, key) }
        : { [key]: val };
  }

  return typeof def.default !== 'undefined'
      ? { [key]: def.default }
      : {};
};

const safeSelector = <D>(definitions: D, query: Query) =>
  Object.keys(definitions).reduce(
    (result, key) => Object.assign(result, keySelector(definitions, query, key)),
    {} as SelectorResult<D>);

const selector = <D>(definitions: D, query: Query = {}) =>
  safeSelector(definitions, query || {});

这些是用于避免回归的单元测试:

// Tests (with colors for Chrome/Firefox JavaScript Console)
test('Skip key not defined', {}, { a: 1 }, {});
test('Empty definition', { a: {} }, { a: 1 }, {});
test('Use default when key is missing', { a: { default: 10 } }, {}, { a: 10 });
test('Keep key with valid definitions', { a: { default: 10 }, b: { ko: 'def' } }, {}, { a: 10 });

test('Use default when value is not valid ', { a: { default: 10, validation: () => false } }, { a: 1 }, { a: 10 });
test('Use value when it is valid          ', { a: { default: 10, validation: () => true  } }, { a: 1 }, { a: 1 });
test('Use converted value when it is valid', { a: { convert: (a: number, k: string) => `${k}=${a * 2}`, validation: () => true } }, { a: 1 }, { a: 'a=2' });

function test<D>(message: string, definitions: D, query: Query, expected: SelectorResult<D>) {
  const actual = selector(definitions, query);
  const args = [expected, actual].map(x => JSON.stringify(x));
  if (args[0] === args[1]) {
    console.log(`%c ✔️ ${message}`, 'color: Green; background: #EFE;');
  } else {
    console.log(`%c ❌ ${message} KO : Expected ${args[0]}, Received ${args[1]}`, 'color: Red; background: #FEE;');
  }
}

可以使用--strictNullChecks之类的每个“严格”编译器选项在TypeScript游乐场here中对代码进行评估和执行。