Need help defining objects with many properties of either type X or Void->X

Apologies for the incomprehensible title. I’m really struggling to put into words the actual problem!

I’m designing a system for creating instances in a game using typedefs to define a function’s parameters.
Some instances have static properties, some have dynamic properties.
(I use the terms static and dynamic in the literal sense, not by their common meaning in code)

Here’s an example:

typedef CharacterParams = {
	var id:String;
	var name:Dynamic;
	var ?gender:Gender;
	var ?type:CharacterType;
}

class Character {
	public var id(get, null):String;
	private var _name:String;
	public var name(get, never):String;
	public function get_name() { return Reflect.isFunction(_name) ? _name() : _name; }
	public var gender(default, never):Gender;
	public var type(default, never):CharacterType;

	public function new(params:CharacterParams) {
		id = params.id;
		_name = params.name;
		gender = params.gender;
		type = params.type;
	}
}

var player = new Character({
	id: "player",
	name: ()->game.data.player_name
});

var jim = new Character({
	id: "jim",
	name: "Jimmy"
});

jim’s name property will always be ‘jim’, but player’s name will be whatever the user decides earlier in the game. That’s what I mean by static and dynamic. The same property in one instance is defined by a function, in the other it’s a string… but when I access Character.name, I just want a string.

It doesn’t strike me as the best code, but it works… but now consider that the real game has a lot more properties in CharacterParams (and many other similarly defined Params typedefs), with the same problem demonstrated with the name.

It quickly becomes a bit of a copy/pasted mess with a ton of repetition, and the thought occurs that I’ve picked a pretty bad system for defining characters.
Another thought occurs that maybe this could all be made much simpler with macros, but I’m new to Haxe and have very little experience with them.

It’s a very broad question, but how could I make this code simpler, and utilizes some of Haxe’s strengths?

Then isn’t that fixed? They decided it previously, and aren’t going to change it, so couldn’t you just store it as a string?

That’s fair. You still need to solve the problem, whether or not it applies to name in particular.


My approach would be to store them all as functions. That way, there would be no need for Reflect.

public var name:() -> String;

//...

character.name = () -> game.data.player_name;
trace(character.name());

Next, adding implicit casts would make it easier to use:

abstract StringGetter(() -> String) from () -> String {
    @:from private static inline function fromString(value:String):StringGetter {
        return () -> value;
    }
    
    @:to private inline function toString():String {
        return this();
    }
}

//...

public var name:StringGetter;

//...

character.name = "Jimmy";
trace(character.name); //The function or function ID, depending on target.

var nameString:String = character.name;
trace(nameString); //"Jimmy"

character.name = () -> game.data.player_name;
trace((character.name:String)); //The current value of `player_name`

And finally, I’d make it generic:

abstract Getter<T>(() -> T) from () -> T {
    @:from private static inline function fromT<T>(value:T):Getter<T> {
        return () -> value;
    }
    
    @:to private inline function toT():T {
        return this();
    }
}