Typedefs
Understand how to create type aliases in Dart.
What is it?
Typedefs (type aliases) allow you to create alternative names for existing types. They make your code more readable by giving meaningful names to complex type signatures, especially for function types, generics, and long type declarations.
Why does it exist?
Typedefs exist to:
- Create meaningful names for complex types
- Reduce repetitive type annotations
- Improve code readability and maintainability
- Simplify function type declarations
- Make code more self-documenting
- Enable easier refactoring
Basic Typedefs
Simple Type Alias
// Simple typedef
typedef Name = String;
typedef Age = int;
typedef Email = String;
typedef PhoneNumber = String;
// Usage
void registerUser(Name name, Age age, Email email) {
print('Registering $name ($age) with email $email');
}
// More readable than:
void registerUser(String name, int age, String email) {
print('Registering $name ($age) with email $email');
}
Generic Typedefs
// Generic typedef
typedef Pair<T> = (T, T);
typedef Triple<T> = (T, T, T);
typedef Result<T> = (bool, T?);
// Usage
Pair<int> coordinates = (10, 20);
Triple<String> names = ('Alice', 'Bob', 'Charlie');
Result<double> calculation = (true, 3.14);
print('Coordinates: ${coordinates.$1}, ${coordinates.$2}');
print('Names: ${names.$1}, ${names.$2}, ${names.$3}');
print('Result: ${calculation.$1}, ${calculation.$2}');
Collection Typedefs
// Collection aliases
typedef UserList = List<String>;
typedef ScoreMap = Map<String, int>;
typedef StringSet = Set<String>;
// Usage
UserList users = ['Alice', 'Bob', 'Charlie'];
ScoreMap scores = {'Alice': 95, 'Bob': 87, 'Charlie': 92};
StringSet uniqueNames = {'Alice', 'Bob', 'Charlie'};
void printUsers(UserList list) {
for (var user in list) {
print(user);
}
}
void printScores(ScoreMap map) {
map.forEach((name, score) {
print('$name: $score');
});
}
Function Typedefs
Basic Function Types
// Function typedef
typedef IntOperation = int Function(int a, int b);
typedef StringCallback = String Function(String value);
typedef VoidCallback = void Function();
// Usage
IntOperation add = (a, b) => a + b;
IntOperation multiply = (a, b) => a * b;
StringCallback toUpper = (s) => s.toUpperCase();
VoidCallback printHello = () => print('Hello');
print(add(3, 5)); // 8
print(multiply(3, 5)); // 15
print(toUpper('hello')); // HELLO
printHello(); // Hello
Function Types with Parameters
// Complex function typedefs
typedef AsyncCallback<T> = Future<T> Function();
typedef ErrorHandler = void Function(String error, StackTrace? stackTrace);
typedef DataTransformer<T, R> = R Function(T data);
typedef Predicate<T> = bool Function(T item);
// Usage
AsyncCallback<String> fetchData = () async {
await Future.delayed(Duration(seconds: 1));
return 'Data loaded';
};
ErrorHandler handleError = (error, stack) {
print('Error: $error');
if (stack != null) print('Stack: $stack');
};
DataTransformer<String, int> stringLength = (s) => s.length;
Predicate<int> isEven = (n) => n % 2 == 0;
// Use them
fetchData().then(print); // Data loaded
handleError('Something went wrong', null);
print(stringLength('Hello')); // 5
print(isEven(4)); // true
Advanced Typedefs
Typedefs with Generics
// Generic typedefs
typedef Operation<T> = T Function(T a, T b);
typedef Converter<T, R> = R Function(T input);
typedef Parser<T> = T Function(String input);
// Usage
Operation<int> addInts = (a, b) => a + b;
Operation<double> addDoubles = (a, b) => a + b;
Operation<String> concatenate = (a, b) => a + b;
Converter<String, int> length = (s) => s.length;
Parser<int> parseInt = (s) => int.parse(s);
print(addInts(3, 5)); // 8
print(addDoubles(3.14, 2.86)); // 6.0
print(concatenate('Hello', ' World')); // Hello World
print(length('Hello')); // 5
print(parseInt('42')); // 42
Complex Generic Typedefs
// Complex type aliases
typedef Result<T> = ({bool success, T? data, String? error});
typedef AsyncResult<T> = Future<Result<T>>;
typedef StreamProcessor<T> = Stream<T> Function(Stream<T> input);
typedef Callback<T> = void Function(T value, {int index});
// Usage
Result<String> getData() {
return (success: true, data: 'Hello', error: null);
}
Result<int> parseNumber(String input) {
try {
var value = int.parse(input);
return (success: true, data: value, error: null);
} catch (e) {
return (success: false, data: null, error: e.toString());
}
}
// Processing
var result = getData();
if (result.success) {
print(result.data); // Hello
}
var numberResult = parseNumber('123');
if (numberResult.success) {
print(numberResult.data); // 123
}
Typedefs with Extensions
Combining Typedefs and Extensions
// Typedef with extension
typedef ValidatedString = String;
extension ValidatedStringExtension on ValidatedString {
bool get isValid => isNotEmpty;
String get sanitized => trim();
bool get isEmail => contains('@') && contains('.');
}
// Usage
ValidatedString name = 'Alice';
print(name.isValid); // true
print(name.sanitized); // 'Alice'
print(name.isEmail); // false
ValidatedString email = 'user@example.com';
print(email.isEmail); // true
Typedefs in Classes
Using Typedefs with Classes
typedef Validator<T> = bool Function(T value);
typedef Formatter<T> = String Function(T value);
class FormField<T> {
final T value;
final Validator<T> validator;
final Formatter<T> formatter;
FormField({required this.value, required this.validator, required this.formatter});
bool get isValid => validator(value);
String get formatted => formatter(value);
}
// Usage
var field = FormField<String>(
value: 'Alice',
validator: (v) => v.isNotEmpty,
formatter: (v) => v.toUpperCase(),
);
print(field.isValid); // true
print(field.formatted); // ALICE
Typedefs for Callbacks
Event Handlers
// Event handler typedefs
typedef EventListener<T> = void Function(T event);
typedef EventFilter<T> = bool Function(T event);
typedef EventTransformer<T> = T Function(T event);
class EventEmitter<T> {
final List<EventListener<T>> _listeners = [];
final List<EventFilter<T>> _filters = [];
void addListener(EventListener<T> listener) {
_listeners.add(listener);
}
void addFilter(EventFilter<T> filter) {
_filters.add(filter);
}
void emit(T event) {
// Apply filters
for (var filter in _filters) {
if (!filter(event)) return;
}
// Notify listeners
for (var listener in _listeners) {
listener(event);
}
}
}
// Usage
var emitter = EventEmitter<String>();
emitter.addListener((event) => print('Received: $event'));
emitter.addFilter((event) => event.isNotEmpty);
emitter.emit('Hello'); // Received: Hello
emitter.emit(''); // No output (filtered)
Typedefs for State Management
State Management Patterns
// State management typedefs
typedef StateReducer<T> = T Function(T state, dynamic action);
typedef StateSelector<T, R> = R Function(T state);
typedef SideEffect<T> = void Function(T state);
class Store<T> {
final T state;
final StateReducer<T> reducer;
Store(this.state, this.reducer);
T dispatch(dynamic action) {
return reducer(state, action);
}
R select<R>(StateSelector<T, R> selector) {
return selector(state);
}
}
// Usage
class CounterState {
final int count;
CounterState(this.count);
}
CounterState counterReducer(CounterState state, dynamic action) {
if (action == 'increment') {
return CounterState(state.count + 1);
}
if (action == 'decrement') {
return CounterState(state.count - 1);
}
return state;
}
var store = Store(CounterState(0), counterReducer);
print(store.select((state) => state.count)); // 0
store.dispatch('increment');
var newState = store.dispatch('increment'); // But state is immutable
print(newState.count); // 2
Best Practices
Use Descriptive Names
// Good: Descriptive typedef names
typedef UserId = String;
typedef ProductCode = String;
typedef EmailAddress = String;
typedef PhoneNumber = String;
// Bad: Vague names
typedef S = String;
typedef N = int;
typedef L = List;
Use for Complex Function Types
// Good: Clear function typedef
typedef AsyncDataFetcher<T> = Future<T> Function();
typedef ErrorHandler = void Function(Exception error);
typedef DataTransformer<T, R> = R Function(T input);
// Bad: Complex inline types
Future<String> Function() fetchData = () async => 'data';
void Function(Exception) handleError = (e) => print(e);
Use for API Boundaries
// Good: Typedefs for API contracts
typedef ApiResponse<T> = ({bool success, T? data, String? error});
typedef ApiCallback<T> = void Function(ApiResponse<T> response);
typedef ApiEndpoint<T> = Future<ApiResponse<T>> Function();
class ApiClient {
Future<ApiResponse<User>> getUser(String id) async {
try {
// Fetch user
return (success: true, data: user, error: null);
} catch (e) {
return (success: false, data: null, error: e.toString());
}
}
}
Common Mistakes
Unnecessary Typedefs
Wrong:
// Unnecessary alias
typedef IntAlias = int;
typedef StringAlias = String;
Correct:
// Use only when meaningful
typedef UserId = String;
typedef Age = int;
Overcomplicating Typedefs
Wrong:
// Overly complex
typedef ProcessResult<T1, T2, T3, R> = R Function(T1, T2, T3);
// Better: Use class or record
typedef ProcessResult = ({int code, String message, dynamic data});
Not Using Typedefs for Repeated Types
Wrong:
// Repeating complex type
Future<void> process1(Future<String> Function() fetcher) { /* ... */ }
Future<void> process2(Future<String> Function() fetcher) { /* ... */ }
Correct:
// Single typedef
typedef DataFetcher = Future<String> Function();
Future<void> process1(DataFetcher fetcher) { /* ... */ }
Future<void> process2(DataFetcher fetcher) { /* ... */ }
Summary
Typedefs make code more readable by giving meaningful names to complex types. They're especially useful for function types, generics, and API contracts.
Next Steps
Now that you understand typedefs, continue to:
Did You Know?
- Typedefs are compile-time only (no runtime overhead)
- They can be generic with type parameters
- Typedefs work with any type (not just functions)
- Function typedefs are called "function type aliases"
- Typedefs can be exported from libraries
- They're resolved at compile time
- Typedefs can be nested and combined