I have an interface like this:
interface ProjectCostData {
purchasePrice: number;
propertyValue: number;
recentlyDamaged: boolean;
}
Now I want to create an interface like this dynamically based on the interface above:
interface ProjectCostDataWithSetters {
purchasePrice: number;
setPurchasePrice: Dispatch<SetStateAction<number>>;
propertyValue: number;
setPropertyValue: Dispatch<SetStateAction<number>>;
recentlyDamaged: boolean;
setRecentlyDamage: Dispatch<SetStateAction<boolean>>;
}
--> Map over the first interface and for each entry A add a new entry B with the key `set${entryKey}` and a type that can be derived from entry A.
Is this possible in TS?
Background: I have a backend based on OpenAPI that provides me the API structure as a .yaml
file. I pass that file through a generator and receive interfaces and functions to talk to the API. In the frontend I am using react context to build a global state. I would like to build my global react context based on the output of the generator.
In typescript 4.0 or older, this isn't possible. Strings, including property names, are either fully known as constants (i.e. "foo"
), one of a set of limited possibilities of constants (i.e. "foo" | "bar"
), or just any string with any content (string
). So you cannot construct new property names based on other property names.
However, Typescript 4.1 (currently beta) has some features that may help here in template literal types
interface ProjectCostData {
purchasePrice: number;
propertyValue: number;
recentlyDamaged: boolean;
}
// Create a new setter property for each key of an interface
type WithSetters<T extends { [k: string]: any }> = T & {
[K in keyof T & string as `set_${K}`]: (newValue: T[K]) => void
}
// Testing
declare const obj: WithSetters<ProjectCostData>
obj.purchasePrice // number
obj.set_purchasePrice(123) // (newValue: number) => void
That:
[K in keyof T & string as `set_${K}`]
Is a bit funky. This PR explains that better than I could.
You could even handle the capitalization change:
interface Capitals { a: 'A', b: 'B', c: 'C' /* ... */, p: 'P' }
type Capitalize<T extends string> =
T extends `${infer First}${infer Rest}`
? First extends keyof Capitals
? `${Capitals[First]}${Rest}`
: T
: T
type SetterName<T extends string> = `set${Capitalize<T>}`
type SetPropName = SetterName<'propName'> // type: "setPropName"
Putting all that together gets a bit tricky, especially on the beta release. But I think, in theory, all the pieces exist to pull off this type transformation.