Would it be possible to have a copy operation like Object.assign(...) to copy only known Properties to the destination?
My sample code looks like:
class A {
foo?: string;
constructor(p: any) {
Object.assign(this, p);
}
}
const instance = new A({
foo: 'test',
bar: 'other'
});
console.log(instance); // yields: A: { "foo": "test", "bar": "other" }
// but i want: A: { "foo": "test" }
I know that typings are removed in JS but wonder if it would still be possible with something like decorators.
Checking with .hasOwnProperty or similar is not an option because it should allow copy of unset properties like in the example above.
There is no concept of reflection or introspection in TypeScript. So one approach would be to add metadata to your type and roll your own version of assign that uses it.
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);
};
}
/** Uses mappedTypes 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; // added for testing
constructor(data: any) {
customAssign(this, data);
}
}
const instance = new A({
foo: 'test',
bar: 'other'
});
console.log(instance); // outputs {another: 1, foo: 'test'} // another defaults to 1 and not accidentally overwritten.
The solution encompasses the following features.
The output matches your desired output. Additionally values won't get overwritten that were assigned by default.
After a little work, I figured out a type "safish" 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 will throw a compile time error if the corresponding type to the string passed to the mappedType decorator doesn't match a type that the type of the decorated property extends (Dang! That's a confusing sentence). The follow example should help illustrate what I mean.
class A {
@mappedType('string') a?: string; // okay
@mappedType('number') b: number = 1; // okay
@mappedType('object') c?: AnotherClass; // okay
@mappedType('object') d: string[] = []; // okay
@mappedType('number') e?: string; // bad!
@mappedType('object') f: number = 1; // bad!
@mappedType('function') g?: AnotherClass; // bad!
@mappedType('string') h: string[] = []; // bad!
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With