How to expose a templated struct to BP? Is it even possible?

I’ve created a Nullable Variable to represent data that could be null/unset; it’s basically a var and an IsSet bool combined into one. This is really useful for data that should track if it’s been actively set or not. For example, if we have a game mode that explicitly sets a game setting, then that’s being dictated from on high and the player can’t change it. Another variable reports Unset, so the player is free to change it for themselves. edit: it’s also useful for representing fields fetched from the DB or json, where they can be null and it can matter whether or not they were null vs present. It’s just generally super useful to track “isset” as part of the variable.

This is better than having magic values like -1 and “” represent unset, which gets ugly fast. And I’d rather not have a separate variable and IsSet for every one of the hundreds of variables I’m tracking, as people forget to manage them together properly.

This is templated because I want to use this for strings, bools, floats, ints, all sorts of enums, vectors, datetimes, etc.

The problem comes in when we want these accessible to the blueprints. I know stuff like TArray is somehow blueprint-exposed and is able to take basically anything as its templated type that UE can use. So that’s at least one example of a Templated class being blueprint exposed. However, I wonder if that’s down to hard internal UE integration that’s not readily available to peasants like me. Someone mentioned to me that you can tell UE to make aliases, but google searching for this has not born fruit.

I have a nullable variant which can change types, and it can kinda be BP-exposed because it’s not a templated class (I have some static functions for accessing its data). It’s funny to me that the NullableVariable, which is known and unchanging type, is harder to expose.

Is there any way to expose NullableVariable? Or am I stuck manually declaring the variable and IsSet separately for every single var?

template<class T>
class FNullableVariable
{
public:
	FNullableVariable() { bSet = false; }
	FNullableVariable(T setVal) { val = setVal; bSet = true; }

	bool operator==(const FNullableVariable& other) const { return (!other.bSet && !bSet) || (bSet && other.bSet && val == other.val); }
	void operator=(const T& other) { val = other; bSet = true; }
	void operator=(const FNullableVariable& other) { val = other.val; bSet = other.bSet; }
	// note there's no operator= to assign a T to a FNullableVar<T>. That's on purpose: they MUST call Get and specify a value if not set.

	/// <summary>
	/// Get the variable's value, or the default value you provide if the var isn't set.
	/// </summary>
	/// <param name="ValueIfNotSet">If the variable isn't set, this will be returned instead.</param>
	/// <returns></returns>
	T GetBlind(T ValueIfNotSet) const
	{
		if (bSet)
			return val;
		else
			return ValueIfNotSet;
	}

	/// <summary>
	/// Get the value with no safety check for whether it's set. Call this if
	/// you're already separately confirming the variable is set before use.
	/// Please don't abuse this function. If you're too lazy to call IsSet, or GetBlind and
	/// provide a ValueIfNotSet, then you're trampling the whole reason to use
	/// a nullable variable in the first place!
	/// </summary>
	T GetWithoutCheck() const
	{
		return val;
	}

	// Check if the variable has been set to something.
	bool IsSet() const { return bSet; }

	// Set the variable to a new value
	void Set(const T& newval) { val = newval; bSet = true; }
	// Mark the variable as no longer set.
	void UnSet() { bSet = false; }

protected:
	T val;
	bool bSet = false;
};

// Example struct using this, which wants to be BP-exposed
struct FGolfWebEventRules
{
public:
	FNullableVariable<FDateTime> CreatedAt;
	FNullableVariable<FDateTime> StartsAt;
	FNullableVariable<FDateTime> EndsAt;
	FNullableVariable<EUserGender> EventForGender;
	FNullableVariable<float> HandicapMin;
	FNullableVariable<float> HandicapMax;
	FGuid OwnerId;
	// If not empty, event only allows these users to enter.
	TArray<FGuid> InvitedUsers;
	FNullableVariable<EEventAccess> Access;
	FNullableVariable<EEventHandicapType> HandicapType;
	FNullableVariable<int32> PlayerCount;
	FNullableVariable<int32> PlayerMax;
	// If not null, specifies that teams are required and how many teams.
	FNullableVariable<int32> TeamsRequired;
	TArray<ESimulator> TrackingSystemsAllowed;
};