[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 TKeys
is ("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;
};
};