Leveraging Delphi Generics: Type‑Safe Lists with TList<T>
Before Delphi 2009, the built‑in TList stored TObject references and forced developers to cast every addition and retrieval, opening the door to runtime type errors. Delphi’s generics, especially TList<T>, let you declare the element type once and get compile‑time safety without extra casting.
11 May 2026, 11:51 UTC

The Problem: Runtime Casts in Legacy Lists
Before Delphi 2009, the only built‑in list type was TList, which stores TObject references. Adding an integer required a cast, and retrieving a value needed another cast, opening the door to silent EInvalidCast exceptions if the types didn’t match.
Thesis: Generics Give Compile‑Time Safety Without Boilerplate
By using TList you declare the element type once, and the compiler enforces that only compatible values can be added or read. This eliminates the need for manual casting and makes the intent of the code self‑documenting.
How TList Works
The class is declared as TList = class(TObject) where T is a type parameter. When you write var Numbers: TList;, the compiler creates a specialized version of the class that treats T as Integer at runtime. No casting is performed; the Add method expects an Integer and the Item property returns one.
Applying Generic Constraints
Sometimes you need a list that can only hold objects with a certain capability. Generic constraints let you express that directly. For example, a list of components that must descend from TCustomControl can be written as:
type
TControlList<T: TCustomControl> = class(TList)
end;
If you try to instantiate TControlList<TForm> the code compiles because TForm inherits from TCustomControl. Attempting TControlList<Integer> triggers a compile‑time error: “[dcc32 Error] E2003 Undeclared identifier: ‘Integer’” (or similar, depending on version).
Worked Example: A Type‑Safe Event Handler List
Suppose you want to keep a list of callback objects that implement an INotify interface. Using a constrained generic list guarantees that every stored object can receive the notification.
type
INotify = interface
['{FA123456-7890-ABCD-EF12-3456789ABCDE}']
procedure Notify;
end;
TNotifierList<T: INotify> = class(TList)
public
procedure NotifyAll;
end;
{ Implementation }
procedure TNotifierList<T: INotify>.NotifyAll;
var
I: Integer;
begin
for I := 0 to Count - 1 do
Items[I].Notify;
end;
{ Usage }
var
List: TNotifierList<TForm>;
begin
List := TNotifierList<TForm>.Create;
try
List.Add(Form1); // OK if Form1 implements INotify
List.Add(Form2);
List.NotifyAll; // Calls Notify on each form
finally
List.Free;
end;
end;
The compiler will reject List.Add(123) because an Integer does not implement INotify. This catches the mistake at build time rather than waiting for a runtime exception.
Trade‑Off: Binary Size and Compilation Time
Each distinct specialization of a generic type generates its own code at runtime. If you create many different TList<X> types (e.g., for dozens of record or class types), the executable can grow noticeably and the compiler takes longer to process the unit. In practice, the impact is modest for a handful of specializations, but developers working on large libraries should profile the binary size or use the -$L linker option to see duplicate code.
Actionable Closing
Start by replacing any legacy TList that holds a known concrete type with TList<T>. Add a constraint only when you need to call members specific to that type. Verify the change by attempting to add an incompatible value; the compiler should emit a clear error. This small refactor yields immediate safety gains with minimal runtime overhead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.