The C Path — learn C, visually

🧬 Types & Qualifiers, In Depth

typedef: giving types better names

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Some of C's most familiar type names — size_t, or the FILE behind every file you'll ever open — aren't built into the language at all: they're nicknames minted with typedef, and this lesson shows you the trick. You'll also use it to turn a declaration full of stars and parentheses into a name a human can read at a glance.

typedef creates an alias for an existing type — a nickname, not a new type. typedef int Celsius; makes Celsius and int fully interchangeable (the compiler won't stop you mixing them — it's documentation, not a wall between types). Where typedef shines is taming C's gnarlier type syntax.

The one insight that makes typedef easy

A typedef is written exactly like a variable declaration, with typedef stapled on front. Wherever the variable name would be, that becomes the type name:

mirror.c — declaration, then typedef
/* variable declarations...          ...become type aliases */
unsigned char      byte_v;    typedef unsigned char      byte;
int                pair_v[2]; typedef int                pair[2];
int              (*cmp_v)(const void *, const void *);
typedef int      (*cmp_fn)(const void *, const void *);

/* now these are easy to read: */
byte   b   = 0xFF;     /* one unsigned char        */
pair   xy  = { 3, 4 }; /* an array of two ints!    */
cmp_fn f;              /* pointer to qsort-style fn */

This mirror rule works for arbitrarily hairy types. Can you declare it as a variable? Then you can typedef it.

🧠 Checkpoint: After typedef int Meters;, what does Meters m = 5; int x = m; do?

  • Compile error: incompatible types
  • Compiles: Meters IS int, just under another name
  • Runtime conversion from Meters to int
  • Undefined behavior
Show answer

Compiles: Meters IS int, just under another name — typedef creates an alias, not a distinct type. Great for readability and portability, useless as a type-safety wall — the compiler sees int everywhere.

Taming structs and function pointers

The two heavyweight champions of "please give this a name":

tamed.c
#include <stdio.h>
#include <stdlib.h>

typedef struct Node Node;     /* name first: usable below */
struct Node {
    int   value;
    Node *next;               /* thanks to the line above  */
};

typedef int (*cmp_fn)(const void *, const void *);

int by_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main(void) {
    cmp_fn cmp = by_int;      /* vs: int (*cmp)(const void*, const void*) */
    int v[] = { 31, 4, 15 };
    qsort(v, 3, sizeof v[0], cmp);
    printf("%d %d %d\n", v[0], v[1], v[2]);

    Node n2 = { 20, NULL }, n1 = { 10, &n2 };
    printf("%d -> %d\n", n1.value, n1.next->value);
    return 0;
}
terminal
$ gcc tamed.c -o tamed && ./tamed
4 15 31
10 -> 20

Note the self-referencing struct: the struct Node tag is still needed inside, because the typedef name isn't usable until the declaration ends. The idiom typedef struct Node Node; before the struct body sidesteps this entirely.

🧠 Checkpoint: Inside struct Node { ... };, why can't the member be declared with a typedef name defined after the struct?

  • Members can never be pointers to the own struct
  • A typedef name only exists once its declaration is complete — inside the body you must use the struct tag (or pre-declare the typedef)
  • typedef and struct cannot mix
  • It can — order never matters in C
Show answer

A typedef name only exists once its declaration is complete — inside the body you must use the struct tag (or pre-declare the typedef) — C is processed top-to-bottom. Either use the tag (struct Node *next;) or write typedef struct Node Node; BEFORE the body — then Node *next; works inside it.

Opaque types: typedef as API armor

Here's a professional trick. Declare a typedef of an incomplete struct in the header, and define the struct only in the .c file. Users can hold pointers to a Timer, but can't see or touch its fields — the layout can change without breaking anyone:

timer.h + timer.c — the opaque pattern
/* ---- timer.h (what users see) ---- */
typedef struct Timer Timer;        /* incomplete: fields hidden */

Timer *timer_start(const char *name);
double timer_elapsed(const Timer *t);
void   timer_free(Timer *t);

/* ---- timer.c (private) ---- */
struct Timer {                      /* real definition lives here */
    const char *name;
    long        start_ns;
};
/* users CANNOT write t->start_ns — incomplete type! */
💡

This is exactly how FILE works in <stdio.h>: you juggle FILE * pointers everywhere but have (portably) no idea what's inside a FILE. That's an opaque type in the wild — and why the pattern is also called the "FILE idiom".

When NOT to typedef: hiding pointers

🤔 Given typedef char *string; — what exactly does const string s declare?

Think first

It declares char *const s — a const pointer to modifiable chars. Not const char *s! The qualifier applies to the typedef'd type as a sealed unit; const cannot reach "inside" the alias to qualify the pointee. This trap alone is a good reason not to hide pointers behind typedefs.

⚠️

Rule of thumb: don't typedef away a * unless the type is truly opaque (a handle). typedef char *string; looks cute, but readers can no longer see that assignment copies a pointer (not the text!), and const stops meaning what they think. The Linux kernel style guide bans pointer typedefs for exactly this reason.

🧠 Checkpoint: When is typedef-ing a pointer type considered good style?

  • Always — it saves keystrokes
  • For opaque handles where callers should never dereference it anyway
  • Whenever the pointer is const
  • Never, it is a syntax error
Show answer

For opaque handles where callers should never dereference it anyway — If users are meant to treat the value as a black-box handle, hiding the * is honest. If they will dereference, index, or free it, hiding its pointer-ness just obscures the code and breaks const intuition.

You use typedefs constantly already

size_t, ptrdiff_t, uint32_t, sig_atomic_t, FILE, time_t — all typedefs. That's the portability trick of the standard library: uint32_t might alias unsigned int on your machine and unsigned long on another, but your code just says uint32_t and works everywhere.

This spot has an interactive editor widget — open the interactive lesson to play with it.

typedef names existing types — next up is the keyword that mints whole families of named integer constants: enum.

▶ Practice this lesson interactively (with live gcc)