简体   繁体   中英

Convert the function to use typescript types

I have of following function:

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

This typescript is report me that a need to type correcty definitions and query but i really don't know how.

Error Message. Element implicitly has an 'any' type because type '{}' has no index signature.

How can I do it?


Usage:

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)

Test

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

Case of usage: https://jsbin.com/gatojir/edit?js,console

Note: validation is required, but convert , and default are optional parameters.

An index signature is when you specify that the various keys of an object will correspond to a certain type, even though you don't know ahead of time exactly which keys those will be (or don't want to enumerate them).

For example, your definitions parameter might be defined something like this:

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
}

Be aware that your code has another problem: You're calling definitions[key].validation(query[key]) without making sure that definitions[key].validation is defined first. You describe it as being required, but then your own example shows that it may not exist, since limit has no validation function. If you need to support the case where validation doesn't exist, you'll need to add that check in:

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

nicholas provided a good solution for the definition argument. We can enhance it a bit to be generic and more explicit, and give a type for the query argument:

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

Notes:

  • The type alias QueryValue helps clarifying where a value from the query is used in a definition .
  • definitions type will not have the type definitionsCollection suggested by nicholas but a generic type D . It's less user-friendly when calling the selector() function the but it's the only way to infer the value type for each keys.

For the return type (currently {} !), it's a real challenge. We need to select only some keys in both the definitions and the query , some of these keys being optional, depending of the result of validation() method of the definition.

The basic idea is to create a mapped type like Partial , combined with some conditional types criteria with type inference.

To select the keys, we cannot play with the never type for the value: the type { a: never } is not reduced to {} . We have to filter the keys "inline", recompose the object with the filtered keys and extract these keys! I took this idea from the FunctionPropertyNames<T> found amongst distributive conditional types .

To differentiate keys that are optional, we have no choice but splitting the definition in 2 and recombining them in an intersection type: type T = { K: V } & { K?: V } . Indeed, { K: V|undefined } is not equivalent to { K?: V } .

With some helper types and handling different cases, the result is:

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

We can use some "test types" that can be evaluated in the editor to validate the SelectorResult type. The results are good, but it would have been better if the intersection types have been merged:

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

The type usage can be found in the following code, with a little refactoring to be a bit more functional:

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

These are the unit tests used to avoid regressions:

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

The code can be evaluated and executed in the TypeScript Playground here with every "strict" compiler options like --strictNullChecks .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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