VBA User-Defined Types

User-defined types are composite data types containing one or more variables of other data types. User-defined types can be used to return multiple values from a function, pass a struct to a DLL procedure, improve efficiency when used in place of a class, and to make code more organized and readable.

User-Defined Types Vs Classes

Although user-defined types can sometimes take the place of using a class, they have several key differences from classes:

  • Classes can define methods, user-defined types cannot
  • Classes can be instantiated, user-defined types cannot
  • Classes require their own dedicated modules, user-defined types do not
  • Class instances can be added to a Collection object, user-defined types cannot
  • User-defined types are more efficient than classes

Although there are some limitations with user-defined types when compared to classes, user-defined types can be a more efficient and light-weight alternative when all that is needed is a structure containing data fields. User-defined types do not have the overhead of a dedicated module and can help keep the size of a VBA project small when defining a number of data structures. Using a user-defined type with an array is more efficient than using a user-defined class with a Collection object. If a data structure does not need methods or instances then one should consider using a user-defined type instead of a class.

Declaring User-Defined Types

User-defined types are declared using the Type statement. There are a number of rules when declaring user-defined types:

  • Must be declared above all procedures in a module.
  • Must have at least one member.
  • When Public or Private is not specified, user-defined types are Public by default.
  • Public types cannot be declared in class modules.
  • Private types in a class module cannot be used as parameters for public procedures.
  • Private types in a class cannot be return types for public procedures of the class.
  • Private types in class modules cannot be public member variables of the class.
  • Types cannot contain a member that is declared as its own type. No circular dependencies.
  • When a type contains another type as a member, the member's type must be declared first.
'Standard Module

Option Explicit

Public Type TExample1
    Message As String
    Number  As Double
End Type

Public Type TExample2
    Message As String
    T1      As TExample1
End Type

Public Sub Example()

    Dim T1 As TExample1
    T1.Message = "MT1 Message"
    T1.Number = 3.5

    Dim T2 As TExample2
    T2.Message = "T2 Message"
    T2.T1 = T1

    Debug.Print T2.Message, T2.T1.Message, T2.T1.Number
    
End Sub

In a standard code module, Private user-defined types can be accessed publicly as a member variable of a Public user-defined type.

'Standard Module: Module1

Option Explicit

Private Type TPrivate
    Message  As String
    Number   As Double
End Type

Public Type TPublic
    Message  As String
    Priv     As TPrivate
End Type
'Standard Module: Module2

Option Explicit

Public Sub Example()

    Dim T As TPublic

    T.Priv.Message = "Hello, World!"
    T.Priv.Number = 3.5

    Debug.Print T.Priv.Message
    Debug.Print T.Priv.Number

End Sub

Multiple Return Values

There are times when it may be necessary for a function to return more than one value. A user-defined type can be used to return multiple values from a function.

Option Explicit

Public Type TExample
    Message As String
    Number  As Double
End Type

Public Function GetData() As TExample
    GetData.Message = "Hello, World!"
    GetData.Number = 3.5
End Function

Public Sub Example()

    Dim E As TExample
    E = GetData()

    Debug.Print E.Message
    Debug.Print E.Number

End Sub

User-Defined Type Parameter

User-defined types can be used as parameters of procedures.

Option Explicit

Public Type TExample
    Message As String
    Number  As Double
    Bool    As Boolean
End Type

Public Sub PrintTExample(T As TExample)
    Debug.Print T.Message, T.Number, T.Bool
End Sub

Public Sub Example()

    Dim T As TExample

    T.Message = "Hello, World!"
    T.Number = 3.5
    T.Bool = True

    PrintTExample T

End Sub

Pass Struct To DLL Procedure

User-defined types can be used to pass or return structs to and from DLL procedures.

// C++ 64-bit DLL

#define DLLExport __declspec(dllexport)

struct RECTANGLE {
    int s1;
    int s2;
};

extern "C" {

    DLLExport int __stdcall CppRectangleArea(RECTANGLE* rect) {
        return rect->s1 * rect->s2;
    }

}
Option Explicit

Public Type TRectangle
    S1 As Long
    S2 As Long
End Type

Public Declare PtrSafe Function CppRectangleArea _
    Lib "C:\ExampleDLL.dll" (ByRef Rect As TRectangle) As Long

Public Sub Example()

    Dim Rect As TRectangle
    Rect.S1 = 10
    Rect.S2 = 20

    Dim Area As Long
    Area = CppRectangleArea(Rect)

    Debug.Print Area

End Sub

Arrays and User-Defined Types

Using an array with a user-defined type is an excellent way of storing tabular data in a type-safe, meaningful, and memory efficient way. Using a user-defined type array instead of a multi-dimensional array of type Variant provides the correct explicit data type for each field, a clear meaning for what each field represents, and saves memory when compared to using Variants. Consider the example below using a multi-dimensional array of type Variant and then again using an array with a user-defined type.

Note: Arrays of user-defined types cannot be iterated over using a For Each loop.

Option Explicit

Public Sub Example()

    Dim PersonInfoArray(0 To 2, 0 To 2) As Variant

    PersonInfoArray(0, 0) = "Bill"
    PersonInfoArray(0, 1) = #01/01/1992#
    PersonInfoArray(0, 2) = "New York"

    PersonInfoArray(1, 0) = "Bob"
    PersonInfoArray(1, 1) = #01/01/1993#
    PersonInfoArray(1, 2) = "Chicago"

    PersonInfoArray(2, 0) = "Ben"
    PersonInfoArray(2, 1) = #01/01/1994#
    PersonInfoArray(2, 2) = "Los Angeles"

    Dim i As Long
    For i = LBound(PersonInfoArray, 1) To UBound(PersonInfoArray, 1)
        Debug.Print PersonInfoArray(i, 0), PersonInfoArray(i, 1), PersonInfoArray(i, 2)
    Next i

End Sub
Option Explicit

Public Type TPersonInfo
    FirstName As String
    BirthDate As Date
    HomeTown  As String
End Type

Public Sub Example()

    Dim PersonInfoArray(0 To 2) As TPersonInfo

    With PersonInfoArray(0)
        .FirstName = "Bill"
        .BirthDate = #01/01/1992#
        .HomeTown = "New York"
    End With

    With PersonInfoArray(1)
        .FirstName = "Bob"
        .BirthDate = #01/01/1993#
        .HomeTown = "Chicago"
    End With

    With PersonInfoArray(2)
        .FirstName = "Ben"
        .BirthDate = #01/01/1994#
        .HomeTown = "Los Angeles"
    End With

    Dim i As Long
    For i = LBound(PersonInfoArray) To UBound(PersonInfoArray)
        With PersonInfoArray(i)
            Debug.Print .FirstName, .BirthDate, .HomeTown
        End With
    Next i

End Sub

LSet

LSet can be used with User-Defined Types to assign the values in one type with the values of another type.

Note: It is not recommended to use LSet with user-defined types because there is no guarantee that the members of both types are the same.

Option Explicit

Public Type TExample1
    Message As String * 15
    Number  As Double
End Type

Public Type TExample2
    Message As String * 15
    Number  As Double
End Type

Public Sub Example()

    Dim T1 As TExample1
    T1.Message = "Hello, World!"
    T1.Number = 3.5

    Dim T2 As TExample2
    LSet T2 = T1

    Debug.Print T2.Message
    Debug.Print T2.Number

End Sub
Option Explicit

'''VBA does not ensure that user-defined types match

Public Type TExample1
    Message As String * 15
    Number  As Double
End Type

Public Type TExample2
    Number  As Long
    Message As String * 15
End Type

Public Sub Example()

    Dim T1 As TExample1
    T1.Message = "Hello, World!"
    T1.Number = 3.5
    
    Dim T2 As TExample2
    LSet T2 = T1
    
    Debug.Print T2.Number  'Prints: '6619208
    Debug.Print T2.Message 'Prints: 'llo, World!

End Sub