[Typescript] Clean type

const pick = <TObj, TKeys extends (keyof TObj)[]>(obj: TObj, picked: TKeys) => {
  return picked.reduce((acc, key) => {
    acc[key] = obj[key];
    return acc;
  }, {} as Pick<TObj, TKeys[number]>);
};

Notice that TKeysis ("a" | "b")[], which is not really good, better to be "a" | "b".

 

Updated:

const pick = <TObj, TKeys extends keyof TObj>(obj: TObj, picked: TKeys[]) => {
  return picked.reduce((acc, key) => {
    acc[key] = obj[key];
    return acc;
  }, {} as Pick<TObj, TKeys>);
};

 


 

Another example

const makeFormValidatorFactory =
  <TValidators extends Record<string, (value: string) => string | undefined>>(
    validators: TValidators
  ) =>
  <TConfig extends Record<string, Array<keyof TValidators>>>(
    config: TConfig
  ) => {
    return <TValues extends Record<keyof TConfig, string>>(values: TValues) => {
      const errors = {} as { [Key in keyof TValues]: string | undefined };

      for (const key in config) {
        for (const validator of config[key]) {
          const error = validators[validator](values[key]);
          if (error) {
            errors[key] = error;
            break;
          }
        }
      }

      return errors;
    };
  };

const createFormValidator = makeFormValidatorFactory({
  required: (value) => {
    if (value === '') {
      return 'Required';
    }
  },
  minLength: (value) => {
    if (value.length < 5) {
      return 'Minimum length is 5';
    }
  },
  email: (value) => {
    if (!value.includes('@')) {
      return 'Invalid email';
    }
  },
});

const validateUser = createFormValidator({
  id: ['required'],
  username: ['required', 'minLength'],
  email: ['required', 'email'],
});

it('Should properly validate a user', () => {
  const errors = validateUser({
    id: '1',
    username: 'john',
    email: 'Blah',
  });

  expect(errors).toEqual({
    username: 'Minimum length is 5',
    email: 'Invalid email',
  });

  type test = Expect<
    Equal<
      typeof errors,
      {
        id: string | undefined;
        username: string | undefined;
        email: string | undefined;
      }
    >
  >;
});

it('Should not allow you to specify a validator that does not exist', () => {
  createFormValidator({
    // @ts-expect-error
    id: ['i-do-not-exist'],
  });
});

it('Should not allow you to validate an object property that does not exist', () => {
  const validator = createFormValidator({
    id: ['required'],
  });

  validator({
    // @ts-expect-error
    name: '123',
  });
});

So what we really want generic to capture?

Actually just the keys required, email, minLength, therefore we can change it to capture keys only:

Next, we do:

  <TConfigs extends Record<string, Array<TValidatorKeys>>>(
    config: TConfigs
  ) => {...}

We do care about id, username, email, because we need to use it later, but we don't really need to capture the whole config object, we can just capture keys.

<TConfigKeys extends string>(
    config: Record<TConfigKeys, Array<TValidatorKeys>>
  ) => {...}

 

Next, we do:

return <TValues extends Record<TConfigKeys, string>>(values: TValues) => {..}

Not necessary to capture it, so we just do:

return (values: Record<TConfigKeys, string>) => {..}

 

Full Code:

const makeFormValidatorFactory =
  <TValidatorKeys extends string>(
    validators: Record<TValidatorKeys, (value: string) => string | void>
  ) =>
  <TConfigKeys extends string>(
    config: Record<TConfigKeys, Array<TValidatorKeys>>
  ) => {
    return (values: Record<TConfigKeys, string>) => {
      const errors = {} as { [Key in TConfigKeys]: string | undefined };

      for (const key in config) {
        for (const validator of config[key]) {
          const error = validators[validator](values[key]);
          if (error) {
            errors[key] = error;
            break;
          }
        }
      }

      return errors;
    };
  };

 

posted @ 2023-03-01 22:30  Zhentiw  阅读(32)  评论(0编辑  收藏  举报