0

OK so I have this function that should receive a key and a value from an object. Different keys have different values types associated with them.

I dont want to have a generic function like:

function updateField(key: string, value: any) {

}

Take for example I have this object here:

interface ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;
}

I want to create a function that update a field in this object, but I want it properly types...

so when I call:

const value = something;
updateField('address', value);

It would throw an error if value is not of type string;

What I have tried:


// First Approach:
type ForField<T extends keyof ISessionSpecific> = {
    field: T;
    value: Required<ISessionSpecific>[T];
};

type Approach1 =
    | ForField<'address'>
    | ForField<'students'>
    | ForField<'date'>
    | ForField<'duration'>
    | ForField<'subject'>;

// Second Approach
type Approach2<T extends ISessionSpecific = ISessionSpecific> = {
    field: keyof T;
    value: T[keyof T];
};

// Testing
const x: [Approach1, Approach2] = [
    { field: 'address', value: 0 }, // Error
    { field: 'address', value: 0 }, // No Error
];

My first approach solves my problem but I think it is too verbose. Because this interface I created is just an example... the actual interface might be much larger. So I was wondering if there is any way that is more elegant to do this

2 Answers 2

2

You can leverage distributive conditional types to get type similar to Approach1 autogenerated:

type MapToFieldValue<T, K = keyof T> = K extends keyof T ? { field: K, value: T[K] } : never;

const foo: MapToFieldValue<ISessionSpecific> = { field: 'address', value: 0 } // Expect error;

The result of MapToFieldValue<ISessionSpecific> will be union equivalent to:

type ManualMap = {
    field: "students";
    value: number[] | undefined;
} | {
    field: "subject";
    value: number | undefined;
} | {
    field: "address";
    value: string | undefined;
} | {
    field: "duration";
    value: string[] | undefined;
} | {
    field: 'date';
    value: Date | undefined;
}

Another approach using mapped type, produces same result (thanks @Titian):

type MapToFieldValue<T> = { [K in keyof T]: { field: K, value: T[K] } }[keyof T]
Sign up to request clarification or add additional context in comments.

1 Comment

This also works, simpler to understand IMO: type MapToFieldValue<T> = { [K in keyof T]: { field: K, value: T[K] } }[keyof T]
1
interface ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;
}

function updateField<T, P extends keyof T>(obj: T, prop: P, value: T[P]) {
    obj[prop] = value;
}

var myObj: ISessionSpecific = {};


updateField(myObj, "date", new Date()); // OK!
updateField(myObj, "nonExistingProp", "some Value"); // Error "nonExistingProp" is not a valid prop.

let subject1 = 10; // inferred type : number
let subject2 = "10"; // inferred type : string

updateField(myObj, "subject", subject1); // OK!
updateField(myObj, "subject", subject2); // Error Argument of type 'string' is not assignable to parameter of type 'number | undefined'.
updateField(myObj, "subject", undefined); // OK because ISessionSpecific has subject as optional

// if you want the 3rd paramater to be not null or undefined you need to do this:
function updateFieldNotUndefined<T, P extends keyof T>(obj: T, prop: P, value:  Exclude<T[P], null | undefined>) {
    obj[prop] = value;
}
updateFieldNotUndefined(myObj, "subject", undefined); // ERROR!


// If you want to pass an object of Key-Value pairs:
function updateFieldKeyValuePair<T, P extends keyof T>(
    obj: T, 
    kvp: { prop: P, value:  Exclude<T[P], null | undefined> }
) {
    obj[kvp.prop] = kvp.value;
}


// if you want to put it in a class:
class SessionSpecific implements ISessionSpecific {
    students?: number[];
    subject?: number;
    address?: string;
    duration?: string[];
    date?: Date;

    public updateField<P extends keyof this>(prop: P, value: this[P]) {
        this[prop] = value;
    }
}

playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.