Skip to content

User-Defined Types with typedef

Named aliases, parameterized types, forward declarations, package-level typedefs.

Module 2 · Page 2.5

The Feature That Makes Large Designs Maintainable

Consider a 32-bit SoC that uses a 32-bit address bus everywhere — in the AXI agent, the scoreboard, the register model, the monitor, the coverage model. Every one of these components has declarations like logic [31:0] addr. Now the product team decides to extend the address space to 40 bits for the next silicon revision. Without typedef, you search-replace across dozens of files and hope you didn't miss any. With typedef logic [39:0] addr_t in a package, you change exactly one line.

That's the maintenance story. The readability story is just as important. A function signature like task drive(addr_t addr, data_t data, byte_t strb) is self-documenting. The names tell you what each parameter represents without consulting the module documentation. Compare that to task drive(logic [39:0] addr, logic [31:0] data, logic [3:0] strb) — technically identical, but every reader has to mentally decode the widths.

What typedef Actually Does

A typedef creates a new name for an existing type. It does not create a new type — the compiler treats the alias and the original type as fully interchangeable. addr_t variables can be assigned to logic [31:0] variables without any cast. The alias is purely for human readability and one-place-change maintainability.

This is meaningfully different from how types work in strongly-typed languages like Ada or VHDL where a typedef-equivalent creates a distinct type that cannot be mixed. In SV, typedef is purely a naming convenience — there is no type-safety enforcement between a typedef and its underlying type.

  • Simple aliases — Give descriptive names to bit vectors: addr_t, data_t, byte_t. Width changes propagate from a single definition.
  • Complex type aliases — Name struct, enum, union, and array types so they can be used cleanly in function signatures, port lists, and class fields.
  • Forward declarations — typedef class my_class; declares a class name before its full definition — needed for circular class references.
  • Parameterized types — typedef logic [WIDTH-1:0] data_t; inside a parameterized module creates width-flexible types driven by the module parameter.

Syntax — Every Form You'll Use

SystemVerilog — typedef Syntax Forms
// ── BASIC SYNTAX ──────────────────────────────────────────────────
// typedef existing_type new_name;
 
// ── 1. Simple bit-vector aliases ─────────────────────────────────
typedef logic [7:0]   byte_t;
typedef logic [15:0]  hword_t;
typedef logic [31:0]  word_t;
typedef logic [63:0]  dword_t;
typedef logic [31:0]  addr_t;    // semantic: this is an address
typedef logic [31:0]  data_t;    // semantic: this is data
typedef logic [3:0]   strb_t;    // AXI write strobe
 
// ── 2. Signed integer aliases ─────────────────────────────────────
typedef logic signed [7:0]  int8_t;
typedef logic signed [31:0] int32_t;
 
// ── 3. Array type aliases ─────────────────────────────────────────
typedef byte_t         packet_t [64];    // 64-byte fixed packet
typedef byte_t         dyn_buf_t [];     // dynamic byte buffer
typedef word_t         reg_file_t [32]; // 32-entry register file
 
// ── 4. Typedef for struct (defined before the struct) ─────────────
typedef struct packed {
logic [7:0]  opcode;
logic [31:0] addr;
logic [31:0] data;
} axi_cmd_t;
 
// ── 5. Typedef for enum ───────────────────────────────────────────
typedef enum logic [1:0] {
IDLE, ACTIVE, DRAIN, ERROR
} fsm_state_t;
 
// ── 6. Forward declaration for class (circular reference) ─────────
typedef class axi_transaction;    // declare name before full definition
class axi_scoreboard;
axi_transaction pending [$];   // can reference before axi_transaction is defined
endclass
class axi_transaction;          // now define it
axi_scoreboard  sb_handle;     // can reference scoreboard too
endclass
 
// ── 7. Parameterized module with local typedef ─────────────────────
module fifo #(parameter int DATA_W = 8, DEPTH = 16) (…);
typedef logic [DATA_W-1:0] data_t;   // width-flexible inside module
typedef data_t mem_t [DEPTH];
mem_t fifo_mem;
endmodule
typedef formExampleUse case
Simple type aliastypedef logic [31:0] addr_tReadable port names, one-place width change
Signed aliastypedef logic signed [7:0] int8_tSigned arithmetic with descriptive names
Array aliastypedef byte_t pkt_t [64]Passing arrays cleanly between functions
Struct aliastypedef struct packed {...} cmd_tProtocol transaction types
Enum aliastypedef enum {...} state_tFSM states with type name
Forward class declarationtypedef class my_classResolve circular class dependencies
Parameterized localtypedef logic [W-1:0] data_tInside parameterized modules

Visual — Before and After: What typedef Does for Readability

Without typedef vs With typedef

Without typedefWith typedef (from a shared package)
task drive(logic [39:0] a, logic [31:0] d, logic [3:0] s);task drive(addr_t a, data_t d, strb_t s);
logic [39:0] req_addr, resp_addr;addr_t req_addr, resp_addr;
logic [31:0] mem [256];data_t mem [256];
function logic [31:0] read_reg(logic [39:0] addr);function data_t read_reg(addr_t addr);

Width Change Propagation — The Maintenance Win

ScenarioWithout typedefWith typedef
Address bus 32→40 bitsSearch-replace logic [31:0] in every file — miss one and get a width mismatch bugChange one line: typedef logic [39:0] addr_t
Data bus 32→64 bitsUpdate every function signature, every variable declaration, every portChange one line: typedef logic [63:0] data_t
TID from 4 bits to 8 bitsMultiple replacements, easy to miss edge casestypedef logic [7:0] tid_t

Code Examples — Aliases to Production Packages

Example 1 — Beginner: Basic Type Aliases

Example 1 — typedef Basics
module tb_typedef_basic;
 
// Define aliases — normally these go in a package
typedef logic [7:0]  byte_t;
typedef logic [31:0] addr_t;
typedef logic [31:0] data_t;
typedef int           count_t;    // int alias — for semantics
 
// Use the aliases — identical behavior to the underlying types
addr_t  base_addr  = 32'hA000_0000;
data_t  reg_val    = 32'hCAFE_BABE;
count_t txn_count  = 0;
byte_t  byte_arr [4];
 
// typedef is transparent — you can mix with the original type freely
logic [31:0] raw_addr;
raw_addr = base_addr;   // OK: addr_t is just logic [31:0]
 
// Array alias
typedef byte_t pkt_t [8];    // 8-byte packet type
pkt_t pkt1;
pkt_t pkt2;
 
initial begin
foreach (pkt1[i]) pkt1[i] = i * 16;
pkt2 = pkt1;    // whole-array copy using the type name
 
$display("addr   = 0x%08h", base_addr);
$display("data   = 0x%08h", reg_val);
$display("pkt2[3] = 0x%02h", pkt2[3]);  // 0x30
txn_count++;
$display("count  = %0d",    txn_count);
 
$finish;
end
 
endmodule

Example 2 — Intermediate: Shared Bus Types Package

Example 2 — Production-Style Types Package
// ── bus_types_pkg.sv ─────────────────────────────────────────────
package bus_types_pkg;
 
// ── Fundamental bus widths ────────────────────────────────────
typedef logic [31:0] addr_t;   // 32-bit address bus
typedef logic [31:0] data_t;   // 32-bit data bus
typedef logic [3:0]  strb_t;   // byte enables
typedef logic [7:0]  tid_t;    // transaction ID
typedef logic [1:0]  resp_t;   // AXI response: OKAY/EXOKAY/SLVERR/DECERR
 
// ── Composite types ───────────────────────────────────────────
typedef struct packed {
tid_t  id;
addr_t addr;
data_t data;
strb_t strb;
resp_t resp;
} axi_txn_t;
 
// ── Constants using the types ─────────────────────────────────
const addr_t PERIPH_BASE = 32'hA000_0000;
const addr_t MEM_BASE    = 32'h0000_0000;
 
// ── Array types ───────────────────────────────────────────────
typedef data_t reg_file_t [32];   // 32-entry register file
typedef data_t dyn_data_t [];      // dynamic data array (for bursts)
 
endpackage
 
// ── axi_driver.sv — import and use the package ────────────────────
import bus_types_pkg::*;
 
class axi_driver;
 
task automatic write(
input  addr_t addr,
input  data_t data,
input  strb_t strb,
input  tid_t  id,
output resp_t resp
);
$display("[DRV] WRITE tid=%0h addr=0x%08h data=0x%08h strb=%04b",
id, addr, data, strb);
resp = 2'b00;   // OKAY
endtask
 
endclass

Example 3 — Verification: Parameterized Module with Local typedef

Example 3 — Parameterized typedef in RTL Module
// Parameterized FIFO — internal type adapts to parameters
module sync_fifo #(
parameter int DATA_W = 8,
parameter int DEPTH  = 16,
parameter int ADDR_W = $clog2(DEPTH)
) (
input  logic            clk, rst_n,
input  logic            wr_en, rd_en,
input  logic [DATA_W-1:0] wr_data,
output logic [DATA_W-1:0] rd_data,
output logic            full, empty
);
 
// Local typedefs — adapt to module parameters
typedef logic [DATA_W-1:0] data_t;
typedef logic [ADDR_W-1:0] ptr_t;
 
data_t mem   [DEPTH];   // storage using data_t
ptr_t  wr_ptr = 0;      // write pointer using ptr_t
ptr_t  rd_ptr = 0;
 
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
wr_ptr <= 0; rd_ptr <= 0;
end else begin
if (wr_en && !full)  begin mem[wr_ptr] <= wr_data; wr_ptr <= wr_ptr + 1; end
if (rd_en && !empty) begin rd_ptr <= rd_ptr + 1; end
end
 
assign rd_data = mem[rd_ptr];
assign full    = (wr_ptr + 1 == rd_ptr);
assign empty   = (wr_ptr == rd_ptr);
 
endmodule

Example 4 — Forward Declaration for Circular Class References

Example 4 — Forward Class Declaration
// Problem: Class A references Class B, and Class B references Class A
// Without forward declaration, whichever comes first fails to compile
 
// SOLUTION: forward declare both classes first
typedef class axi_monitor;
typedef class axi_scoreboard;
 
class axi_monitor;
axi_scoreboard sb;          // OK: scoreboard declared above even if not defined yet
 
task capture();
// ... capture logic ...
// sb.check(...)
endtask
endclass
 
class axi_scoreboard;
axi_monitor mon;            // OK: monitor forward-declared
int         mismatch_cnt = 0;
 
function void check(logic [31:0] exp, got);
if (exp !== got) mismatch_cnt++;
endfunction
endclass
 
module tb_forward_decl;
initial begin
axi_monitor     mon = new();
axi_scoreboard  sb  = new();
mon.sb = sb;    // connect after construction
$display("Forward declaration resolved successfully");
$finish;
end
endmodule

Simulation and Synthesis Behavior

No Runtime Cost — Pure Compile-Time Feature

typedef has zero impact on simulation performance or synthesized hardware. It is resolved entirely at compile time — the simulator and synthesis tool see the underlying type. There is no type tag, no runtime type checking, no additional memory. It is purely a pre-processor level naming substitution, similar to a C typedef.

Scope Rules

A typedef is visible only within the scope where it is declared: module, class, package, or compilation unit. Declaring typedefs in a package and importing that package with import pkg::* is the standard pattern for sharing types across an entire design. Without a package, each file needs its own typedef — meaning you're back to the multi-file search-replace problem.

ScopeVisibilityRecommended for
Package-levelAnywhere the package is importedProject-wide bus types — the standard approach
Module-levelInside the module onlyLocal types for parameterized modules
Class-levelInside the class onlyClass-specific internal types
Compilation unitAll files in the same compilation unitSmall projects; not recommended for large designs

Where typedef Shapes Real Verification Architecture

Verification Patterns Using typedef
// ── 1. TRANSACTION CLASS WITH TYPED FIELDS ─────────────────────────
import bus_types_pkg::*;
 
class axi_seq_item;
rand addr_t  addr;
rand data_t  data;
rand strb_t  strb;
rand tid_t   id;
rand resp_t  resp;
// Field names document their purpose; no raw logic[N:0] clutter
endclass
 
// ── 2. SCOREBOARD WITH TYPED REFERENCE MODEL ──────────────────────
class reg_scoreboard;
reg_file_t exp_regs;     // 32-entry data_t array
reg_file_t got_regs;
 
function void compare_all();
foreach (exp_regs[i])
if (exp_regs[i] !== got_regs[i])
$error("reg[%0d] mismatch: exp=%h got=%h", i, exp_regs[i], got_regs[i]);
endfunction
endclass
 
// ── 3. FUNCTION THAT WORKS WITH PROTOCOL TYPE ─────────────────────
function automatic string format_txn(input axi_txn_t t);
return $sformatf("id=%h addr=0x%08h data=0x%08h resp=%02b",
t.id, t.addr, t.data, t.resp);
endfunction
 
// ── 4. CONSTRAINED RAND WITH TYPED FIELDS ─────────────────────────
class burst_txn;
rand dyn_data_t data;  // dynamic array of data_t
rand int unsigned len;
constraint data_sz { len inside {[1:16]}; data.size() == len; }
endclass
 
// ── 5. PORT DECLARATION WITH TYPED SIGNALS ────────────────────────
interface axi_if (input logic clk);
import bus_types_pkg::*;
addr_t  awaddr;    // typed port — width from typedef, not magic number
data_t  wdata;
strb_t  wstrb;
resp_t  bresp;
endinterface

Bugs and Pitfalls With typedef

Bug 1 — typedef Doesn't Enforce Semantic Separation

Bug 1 — No Type Safety Between Aliases
typedef logic [31:0] addr_t;
typedef logic [31:0] data_t;
 
addr_t  req_addr;
data_t  wr_data;
 
// BUGGY: accidentally swapped — passes without any warning
task drive(input addr_t a, input data_t d);
/* ... */
endtask
 
drive(wr_data, req_addr);   // swapped! Compiles and runs silently
// Both are logic[31:0] underneath — no type mismatch detected
 
// PREVENTION: code review + waveform check + naming conventions
// SV typedef cannot prevent this — it's a documentation tool, not a type enforcer

Bug 2 — typedef in Module Without Package: Not Shared

Bug 2 — Module-Scoped typedef Not Visible Elsewhere
// BUGGY: typedef inside a module — not visible to other files
module dut;
typedef logic [31:0] addr_t;   // only visible inside 'dut'
endmodule
 
class axi_driver;
addr_t req_addr;   // COMPILE ERROR: addr_t not visible here
endclass
 
// CORRECT: define typedefs in a package
package types_pkg;
typedef logic [31:0] addr_t;   // visible wherever types_pkg is imported
endpackage
 
import types_pkg::*;
 
class axi_driver_ok;
addr_t req_addr;   // OK: imported from package
endclass

Bug 3 — Width Change Not Propagating: typedef in Wrong Scope

Bug 3 — Not All Files Using the Same typedef
// types_pkg.sv has the canonical typedef
// package types_pkg;
//   typedef logic [31:0] addr_t;   ← needs to become [39:0]
// endpackage
 
// PROBLEM: one file has its own local typedef instead of importing
module old_module;
typedef logic [31:0] addr_t;  // local override — doesn't use the package!
addr_t base = 32'hA000;         // still 32-bit after package was updated to 40-bit
// Width mismatch bug: this module connects to updated 40-bit ports elsewhere
endmodule
 
// PREVENTION: never redeclare typedefs locally if they exist in a package
// Always import: "import types_pkg::*;" — don't copy-paste typedefs
module correct_module;
import types_pkg::*;
addr_t base = 40'hA0_0000_0000;  // automatically 40-bit after package update
endmodule

Interview Questions

Beginner Level

Q1: What does typedef do in SystemVerilog? Does it create a new type?typedef creates a named alias for an existing type. It does not create a new type — the compiler treats the alias and the original as fully interchangeable. You can assign an addr_t to a data_t variable without any cast if both are aliases for the same underlying type. typedef is purely a compile-time naming mechanism with no runtime overhead. Q2: Where is the best place to put typedefs in a large verification project? In a shared package, imported by every file that needs the types. This is the standard professional pattern. The package acts as a single source of truth for type widths — a change propagates to all files automatically. Never duplicate typedefs across files; duplicate definitions diverge when widths change, creating silent mismatch bugs.

Intermediate Level

Q3: What is a forward declaration with typedef and when do you need it?typedef class my_class; declares a class name before its full definition. It is needed when two classes have a circular dependency — Class A contains a handle to Class B, and Class B contains a handle to Class A. Without forward declarations, the compiler sees the second reference before the class is defined and errors. Forward declarations tell the compiler "this name will be defined later, trust me." Both classes must be fully defined before any of their methods can be called.

Experienced Engineer Level

Q4: A project updates the address bus from 32 to 40 bits. The RTL and DUT were updated but the testbench still uses 32-bit addresses in some places. Why didn't the typedef change fix everything? Some testbench files either (1) had their own local typedef declarations instead of importing from the package, (2) used logic [31:0] directly without using the typedef at all, or (3) imported the package but also had local overrides that shadowed the updated typedef. The fix: audit every file for raw logic [31:0] address declarations and replace with addr_t. Enable linting rules that warn on undeclared type usage and prohibit local typedef redefinitions of package-level types.

Best Practices & Coding Guidelines

  • All types in a package — Never scatter typedefs across individual files. One package (project_types_pkg) holds everything. Import everywhere with import pkg::*.
  • Use _t suffix consistently — All type aliases end in _t: addr_t, data_t, txn_t. This instantly distinguishes type names from signal names in code reviews.
  • Never duplicate across files — If a typedef exists in a package, never copy-paste it into a module or class. Always import. Duplicates diverge silently when widths change.
  • Parameterized modules: local typedef — Inside parameterized modules, define local typedefs using parameters: typedef logic [DATA_W-1:0] data_t. This makes the code self-documenting and width-flexible simultaneously.
ScenarioCorrect approachAvoid
Project-wide bus widthOne typedef in a shared packageDuplicating in every file
Parameterized module internal typeLocal typedef using parameterHardcoded widths inside module
Circular class referencestypedef class forward declarationReordering classes (breaks other refs)
Array of structs in a TBtypedef struct {...} txn_t; txn_t q [$];Inline anonymous struct definition
Naming conventionaddr_t, data_t, state_tMixed conventions like ADDR, DataType

Summary

typedef is the simplest feature in this chapter but it has an outsized impact on code quality in large projects. A well-designed types package means width changes cost one edit, port lists are self-documenting, and type mismatches between RTL and TB are caught at compile time rather than hunting mismatch bugs in simulation.

  • typedef creates an alias, not a new type. The compiler treats the alias and original as identical — no type enforcement between semantically different aliases.
  • Put all typedefs in a shared package. This is the only way to guarantee width changes propagate everywhere.
  • Use the _t suffix. It distinguishes type names from signal names instantly and consistently.
  • Local typedef in parameterized modules creates width-flexible internal types that adapt to module parameters.
  • Forward declarations resolve circular class dependencies. Declare with typedef class before the full definition.