Deriving Object Type
An OpaqueObject is one where all of the values are string and some keys are required while others are optional. Let's first define a type assuming that we already know which is which:
type OpaqueObject<AllKeys extends string, RequiredKeys extends string> = Partial<Record<AllKeys, string>> & Record<RequiredKeys, string>
If we want to go from a schema to an object, we know that we need to find all of the keys -- which is easy as it's just keyof Schema -- and the required keys. We know that a key if required if it's value in the schema is true.
Important note: we must use as const when creating a schema in order to separate true from false, otherwise we just know that we have a boolean.
The required keys for a schema S are:
type RequiredKeys<S> = {
[K in keyof S]: S[K] extends true ? K : never;
}[keyof S]
So we can now write a type for an OpaqueObject which depends only on the schema S.
type OpaqueObject<S> = Partial<Record<keyof S, string>> & Record<RequiredKeys<S>, string>
Getting Fields
Now to the getField function. We don't want to pass in a boolean required flag because we should know this already. Instead, let's make this dependent on generics schema S and key K:
function getField<S, K extends keyof S>(
obj: OpaqueObject<S>, field: K
): OpaqueObject<S>[K] {
return obj[field];
}
But honestly this whole function is made unnecessary if we have a properly typed object because we would get the right return type from accessing the property directly.
const exampleSchema = {
f1: true,
f2: false,
} as const;
type ExampleObject = OpaqueObject<typeof exampleSchema>
class OpaqueObjectWrapper {
private obj: ExampleObject
constructor(obj: ExampleObject) {
this.obj = obj;
}
get f1(): string { return this.obj.f1 }
get f2(): string | undefined { return this.obj.f2 }
}
Unknowns
I'm confused about the source of the schemas in regards to whether the as const is possible. Are these coming in as variables from an external source? or are you defining them by writing out objects in your code?
The part that's tripping me up about your question is implementing get f1() and get f2() in a dynamic way inside a class. Unlike other languages like PHP, Javascript doesn't have a dynamic getter where the property value is unknown. You can only do it through a Proxy.
Proxied Objects
The only way that I am aware of for how to dynamically get a property is by using a Proxy. I've got this mostly worked out. The part I am still missing is how to implement a construct signature on the proxied psuedo-class.
This proxy takes an instance of a class which stores an object as property obj and allows us to access properties of obj directly. In order for typescript to understand the added properties, we have to assert as Constructable<T> & T to say that all properties of T can be accessed.
const proxied = <T,>(inst: Constructable<T> ) => {
return new Proxy( inst, {
get: function <K extends keyof T>(oTarget: Constructable<T>, sKey: K): T[K] {
return oTarget.obj[sKey];
},
}) as Constructable<T> & T
}
The underlying class I'm using to store the object is
// stores an object internally, but allows it to be created by calling new()
class Constructable<T> {
private _obj: T
constructor(obj: T) {
this._obj = obj;
}
// object is readonly
get obj(): T {
return this._obj;
}
}
So now we want to make a proxied class based on a schema. We want this:
interface ProxiedConstructable<T> {
// pass in an object T and get something which can access of the properties of T
new( args: T ): Readonly<T>;
}
I told you I wasn't all the way there and it's because the proxy applies to an instance of the class rather than the class itself, so I'm stuck on getting our factory to return something "new-able", but here's what I've got:
const makeProxied = <S extends { [field: string]: boolean }>(schema: S) =>
(obj: OpaqueObject<S>) => {
return proxied( new Constructable(obj) );
}
Which works like this:
const test = makeProxied({
f1: true,
f2: false
} as const);
const testObj = test({f1: "hello world"});
const f1: string = testObj.f1;
const f2: string | undefined = testObj.f2;
Playground Link
get f1()it would betrue, right? - Shivam Singla