I'm trying to write a generic function that can be used to update the string attributes of an interface. In short, given the following:
interface SomeObject {
b1: boolean;
s1: string;
s2: string;
n1: number;
s3: string;
n2: number;
}
const obj = getSomeObject();
const stringResult1 = getResultOf("s1");
obj.s1 = stringResult1;
const stringResult2 = getResultOf("s2");
obj.s2 = stringResult2;
const stringResult3 = getResultOf("s3");
obj.s3 = stringResult3;
I would like to do the following:
interface SomeObject {
b1: boolean;
s1: string;
s2: string;
n1: number;
s3: string;
n2: number;
}
const obj = getSomeObject();
doSomething(obj, "s1");
doSomething(obj, "s2");
doSomething(obj, "s3");
// This should cause a compile error because "n1" is a number.
doSomething(obj, "n1");
// This should cause a compile error because "cx" doesn't exist.
doSomething(obj, "cx");
I've got as far as sorting out the parameter type so that I can restrict the property selection, but the function implementation code is giving the dreaded TS2322 error.
This is as far as I've been able to get so far:
type StringTypeOf<T> = {
[P in keyof T as T[P] extends (string | undefined) ? P : never]: string
}
type StringKeyOf<T> = keyof StringTypeOf<T>;
function doSomething<T>(t: T, key: StringKeyOf<T>) {
// The dreaded TS2322 error.
// Type 'string' is not assignable to type 'T[keyof { [P in keyof T as T[P] extends string | undefined ? P : never]: string; }]'.
// 'T[keyof { [P in keyof T as T[P] extends string | undefined ? P : never]: string; }]' could be instantiated with an arbitrary type which could be unrelated to 'string'.
t[key] = "123";
}
interface Whatever {
a: string;
b?: string;
readonly c: string;
d: number;
e?: boolean;
};
const w: Whatever = {
a: "this",
b: "is",
c: "frustrating",
d: 0
}
// This works, as attribute 'a' is type string.
doSomething(w, "a");
// This works, as attribute 'b' is type string or undefined.
doSomething(w, "b");
// This works, as attribute 'c' is type string, but it would be nice to restrict it because 'c' is readonly.
doSomething(w, "c");
// This fails, as attribute 'd' is type number.
doSomething(w, "d");
// This fails, as attribute 'e' is type boolean or undefined.
doSomething(w, "e");
// This fails, as attribute 'f' doesn't exist.
doSomething(w, "f");
The code is available on TypeScript Playground to show the various errors.
Ideally, the declaration should:
Thanks in advance.
--- EDIT ---
Thanks to the comment from jcalz and the answer from Alexander Nenashev, I've done a "separation of concerns" and come up with what I think is a workable solution here.
You can use an extra logic to filter readonly keys out and use an assertion to cast a string literal:
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
type StringTypeOf<T> = {
[P in keyof T as T[P] extends (string | undefined) ? Equal<Pick<T, P> , Readonly<Pick<T, P>>> extends true ? never : P : never]: string
}
type StringKeyOf<T> = keyof StringTypeOf<T>;
function doSomething<T, K extends StringKeyOf<T>>(t: T, key: K) {
t[key] = 'K' as K;
}