I am having trouble finding the right typing for a function that generates an object with setter and getter properties. I think my best bet is some use of template string literals, but I am having a hard time finding the magic combination of types. In pseudo-code JS, what I would like is something like this:
function createGetterAndSetter( key ) {
return {
[`get${capitalize(key)}`] : ( model ) => {
return model[key]
},
[`set${capitalize(key)}`] : ( model, value ) => {
return {
...model,
[key] : value
}
}
}
}
what I've tried so far is this:
interface Model {
[key:string]: any
}
type ModelKey = keyof Model & string
type Setter = ( model : Model, value : unknown ) => Model
type Getter = ( model : Model ) => unknown
type GetterKey<T extends string> = `get${Capitalize<T>}`
type SetterKey<T extends string> = `set${Capitalize<T>}`
type GetterSetterKeys<T extends string> = GetterKey<T> | SetterKey<T>
type GetterSetter<T extends GetterKey | SetterKey> = Record<GetterSetterKeys<T>, Getter | Setter>
function createGetterAndSetter<T extends ModelKey>( key : ModelKey ) : GetterSetter<T> {
const keyName = `${key[0].toUpperCase() key.substring( 1 )}`
return {
[`set${keyName}`] : createSetValue( key, setOpts ) as Setter,
[`get${keyName}`] : createGetValue( key, getOpts ) as Getter
} as GetterSetter<T>
}
I'm quite new to TS, so this is probably nonsensical in some respects, but it does seem get the compiler to recognize that the return of createGetterAndSetter
is an object with getX and setX keys, it just can't recognize whether the value for those keys is a getter or setter... it only knows it's one of the union of the two.
It may help to know that I was inspired by the createApi
function of redux-toolkit, which is able to create named hooks for you based on your named endpoints.
CodePudding user response:
Here's a possible typing:
type GetterSetter<K extends string> =
{ [P in `get${Capitalize<K>}`]:
<T extends { [Q in K]: any }>(model: T) => T[K]
} &
{ [P in `set${Capitalize<K>}`]:
<T extends { [Q in K]: any }>(model: T, value: T[K]) => T
};
declare const getterSetter: <K extends string>(k: K) => GetterSetter<K>
So getterSetter()
takes a key
of generic type K
and returns a value of type GetterSetter<K>
. This type uses template literal types to make the method names from K
, and the method types are themselves generic so that the return type of getXXX
and the value
parameter of setXXX
are strongly typed with respect to the type of the model
parameter.
The implementation of getterSetter
could be:
const capitalize = (k: string) => k[0].toUpperCase() k.substring(1);
const getterSetter = <K extends string>(k: K) => ({
[`get${capitalize(k)}`]: (m: any) => m[k],
[`set${capitalize(k)}`]: (m: any, v: any) => ({ ...m, [k]: v })
}) as GetterSetter<K>;
Note that I used a type assertion (as GetterSetter<K>
) as well as the any
type inside the implementation. That's because, while it's straightforward-ish to define the GetterSetter<K>
typing, it's a lot trickier to get the compiler to verify that you've implemented it properly. It would need to, among other things, understand that captialize()
performs at a value level the same thing the Capitalize<T>
type function performs at the type level. And it would need to do better typing on computed properties (see ms/TS#13948), etc. It's a lot easier to just use any
and as
and triple check that we didn't do anything wrong, than it is to overcome these hurdles.
Anyway, let's try it out:
const foo = {
prop1: "hey",
prop2: 123,
prop3: true
}
const p1 = getterSetter("prop1");
console.log(p1.getProp1(foo).toUpperCase()); // HEY
p1.getProp1({a: 123}) // error! {a: 123} not expected since it doesn't have a prop1 key
console.log(p1.getProp1({prop1: Math.PI}).toFixed(2)) // 3.14
const bar = p1.setProp1(foo, "you");
console.log(bar.prop1.toUpperCase()) // YOU
Looks good. The value p1
is of type GetterSetter<"prop1">
and therefore has a getProp1()
and a setProp1()
method, each of which behave well. The compiler understands that p1.getProp1({prop1: Math.PI})
is of type number
, and that p1.getProp1({a: 123})
is an error. And the same with setProp1()
.