Alexandre Dias
Alexandre Dias

Reputation: 534

Typescript: Object with keys' types conditioned by other keys of the same object

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

Upvotes: 0

Views: 545

Answers (2)

Aleksey L.
Aleksey L.

Reputation: 38046

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]

Upvotes: 2

Dardino
Dardino

Reputation: 173

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

Upvotes: 1

Related Questions