In TypeScript, there is no built-in support for reflection or introspection. One way to work around this limitation is by adding metadata to your types and creating a custom version of the assign function that utilizes this metadata.
const MAPPED_TYPE_METADATA_KEY = Symbol('mappedType');
type TypeOfType = 'string' | 'number' | 'bigint' | 'boolean' | 'function' | 'object';
/** Decorator that adds mapped types to an object. */
function mappedType(typeName: TypeOfType) {
return function(target: any, propertyKey: string): void {
const typeMap = { ...Reflect.get(target, MAPPED_TYPE_METADATA_KEY), [propertyKey]: typeName };
Reflect.set(target, MAPPED_TYPE_METADATA_KEY, typeMap);
Reflect.defineMetadata(MAPPED_TYPE_METADATA_KEY, typeMap, target);
};
}
/** Custom assignment function that uses mapped types to assign values to an object. */
function customAssign<T>(obj: T, data: Partial<{ [key in keyof A]: A[key] }>): void {
const typeMap: Record<string | number | symbol, TypeOfType> | undefined = Reflect.get(obj, MAPPED_TYPE_METADATA_KEY);
if (typeMap) {
Object.entries(data)
.filter(([key, value]) => typeMap[key as keyof T] === typeof value)
.forEach(([key, value]) => (obj as any)[key] = value);
}
}
class A {
@mappedType('string')
foo?: string;
@mappedType('number')
another: number = 1;
constructor(data: any) {
customAssign(this, data);
}
}
const instance = new A({
foo: 'test',
bar: 'other'
});
console.log(instance); // Output: {another: 1, foo: 'test'}
This solution includes:
- A mapepdType decorator that creates a type map based on field names and their respective value types. However, it does not enforce type checking.
- A customAssign function that incorporates the created type map when assigning values to objects.
- Replacing Object.assign with customAssign in the constructor of class A.
EDIT
I have improved the previous implementation to provide a more type-safe version.
type TypeOfType = 'string' | 'number' | 'bigint' | 'boolean' | 'function' | 'object';
type TypeOfTypeType<T extends TypeOfType> =
T extends 'string' ? string
: T extends 'number' ? number
: T extends 'bigint' ? bigint
: T extends 'function' ? Function
: T extends 'object' ? object
: never;
function mappedType<T extends TypeOfTypeType<TT>, TT extends TypeOfType>(typeName: TT) {
return function<U extends object, V extends keyof U>(target: U, propertyKey: U[V] extends (T | undefined) ? V : never): void {
const typeMap = { ...Reflect.get(target, MAPPED_TYPE_METADATA_KEY), [propertyKey]: typeName };
Reflect.set(target, MAPPED_TYPE_METADATA_KEY, typeMap);
Reflect.defineMetadata(MAPPED_TYPE_METADATA_KEY, typeMap, target);
};
}
This updated approach will trigger a compile-time error if the type specified in the mappedType decorator does not match the actual type of the decorated property. The examples provided demonstrate this concept clearly.