Reputation: 3731
I am trying to write a function that will update a field of an object if a value that was passed in changed versus what's already in the object. The closest thing i've got so far is
type KnownCountryKeys = "defaultCountry" | "country";
type IHasCountry = {
[key in KnownCountryKeys]: Country;
};
export async function maybeUpdateCountry<THasCountry extends IHasCountry>(
dto: THasCountry,
countryKey: KnownCountryKeys,
newCountryCode: string
) {
// Pseudo-Code
// Ideally, typescript could validate at build-time that
// `dto[countryKey]` is in-fact a Country field, from
// the type annotations alone.
if (dto[countryKey].code != newCountryCode) {
const newCountry = //...validateCountry
dto[countryCode] = newCountry;
}
}
My main problem with this approach is that dto
can not have any more fields other than the ones specified in KnownCountryKeys
I get ts2345
, but if I try to convert it to an interface to make the type flexible I get ts2464
. Furthermore, I'd like to delete KnownCountryKeys
because having that whitelist of field names is a code-smell.
My intent is for this function to receive an arbitrary object of any shape, a string key that is validated by TypeScript to be a field of type Country
, and the key of the new country so I can decided whether the country must be updated in an entirely type-safe fashion.
Upvotes: 0
Views: 444
Reputation: 8520
If I understood You properly, You look for something like:
type IHasCountry<K extends KnownCountryKeys> = { [key in K]: Country; };
export function maybeUpdateCountry<K extends KnownCountryKeys>(
dto: IHasCountry<K>, countryKey: K, newCountryCode: string) {
// ...
EDIT:
If you assume that country code is arbitrary string but the DTO should have a field named as country code with type Country, then:
class Country { constructor(readonly name: string) { } };
function maybeUpdateCountry<K extends string, T extends { [key in K]: Country }>(
dto: T, countryKey: K, newCountryCode: string): void {}
const dto1 = { 'pl_PL': new Country('Poland') };
const dto2 = { 'fr_CH': new Country('Switzerland') };
const dto3 = { 'fr_FR': new Country('France') };
maybeUpdateCountry(dto1, 'pl_PL', 'Pologne'); // OK
maybeUpdateCountry(dto2, 'fr_CH', 'Suisse'); // OK
maybeUpdateCountry(dto2, 'fr_FR', 'Suisse');
// ERROR:
// Argument of type '{ fr_CH: Country; }' is not assignable to parameter of type '{ fr_FR: Country; }'.
// Property 'fr_FR' is missing in type '{ fr_CH: Country; }' but required in type '{ fr_FR: Country; }'.
maybeUpdateCountry(dto2, 'test', 'Test');
// ERROR:
// Argument of type '{ fr_CH: Country; }' is not assignable to parameter of type '{ test: Country; }'.
// Property 'test' is missing in type '{ fr_CH: Country; }' but required in type '{ test: Country; }'.
maybeUpdateCountry({ hello: 'bonjour' }, 'test', 'Test');
// ERROR:
// Argument of type '{ hello: string; }' is not assignable to parameter of type '{ test: Country; }'.
// Object literal may only specify known properties, and 'hello' does not exist in type '{ test: Country; }'.
maybeUpdateCountry({ test: 'Not a country' }, 'test', 'Test');
// ERROR:
// Type 'string' is not assignable to type 'Country'
maybeUpdateCountry({ test: new Country('Testland') }, 'test', 'Test'); // OK
Upvotes: 1