How Dart's Type System Works: Inference, Casts, and Generics
A statically typed language checked at compile time
Dart is statically typed with type inference support. Use is/as for type checking and casting, with automatic type promotion after is checks. Generics enable type-safe collections and classes, while typedef lets you name complex function types.
기본 제공 타입
Dart는 정적 타입 언어로, 컴파일 시간에 타입 검사를 수행합니다. 아래는 Dart가 기본으로 제공하는 타입들입니다.
int integer = 42;
double decimal = 3.14;
num number = 10; // int와 double의 상위 타입
String text = '안녕하세요';
bool flag = true;
List<int> numbers = [1, 2, 3];
Map<String, dynamic> person = {'name': '홍길동', 'age': 30};
Set<String> uniqueNames = {'홍길동', '김철수', '이영희'};
Symbol symbol = #symbolName;
특수 타입: void, dynamic, Object
void printMessage() {
print('메시지 출력');
}
dynamic dynamicValue = '문자열';
dynamicValue = 42; // 다른 타입 재할당 가능
Object objectValue = 'Hello';
dynamic vs Object
dynamic은 타입 검사를 완전히 건너뜁니다. Object는 모든 Dart 객체의 최상위 타입이지만 메서드 호출 시 타입 검사가 이루어집니다.
타입 추론 (Type Inference)
var로 선언하면 초기값에서 타입이 자동 결정되며, 이후 다른 타입으로 변경할 수 없습니다.
var name = '홍길동'; // String으로 추론
var age = 30; // int로 추론
var height = 175.5; // double로 추론
var active = true; // bool로 추론
var items = [1, 2, 3]; // List<int>로 추론
var getName = () {
return '홍길동'; // 반환 타입도 추론
};
var people = [ // List<Map<String, Object>>로 추론
{'name': '홍길동', 'age': 30},
{'name': '김철수', 'age': 25},
];
타입 체크 & 캐스팅
is / is! 연산자로 타입을 확인하면, 해당 블록 안에서 자동으로 타입 프로모션이 적용됩니다.
Object value = '문자열';
if (value is String) {
// 이 블록 안에서 자동으로 String으로 캐스팅됨
print('문자열 길이: ${value.length}');
}
if (value is! int) {
print('정수가 아닙니다');
}
as 연산자는 명시적 캐스팅이며, 실패 시 런타임 에러가 발생합니다.
Object value = '문자열';
String text = value as String;
print(text.toUpperCase());
// int number = value as int; // 런타임 에러!
타입 프로모션 (Type Promotion)
is 검사 이후 해당 블록 안에서 자동으로 타입이 승격됩니다. 블록 바깥에서는 원래 타입으로 돌아갑니다.
Object value = '안녕하세요';
if (value is String) {
// 자동으로 String으로 승격
print('대문자: ${value.toUpperCase()}');
print('길이: ${value.length}');
}
// print(value.length); // 에러: Object에는 length가 없음
제네릭 클래스 (Generic Classes)
타입을 매개변수로 받아 재사용 가능한 클래스를 작성할 수 있습니다.
class Box<T> {
T value;
Box(this.value);
T getValue() {
return value;
}
void setValue(T newValue) {
value = newValue;
}
}
void main() {
var stringBox = Box<String>('안녕하세요');
print(stringBox.getValue()); // '안녕하세요'
var intBox = Box<int>(42);
print(intBox.getValue()); // 42
var doubleBox = Box(3.14); // Box<double>로 추론
}
제네릭 함수 (Generic Functions)
T first<T>(List<T> items) {
return items.first;
}
void main() {
var names = ['홍길동', '김철수', '이영희'];
var firstString = first<String>(names);
print(firstString); // '홍길동'
var numbers = [1, 2, 3, 4, 5];
var firstInt = first(numbers); // T가 int로 추론
print(firstInt); // 1
}
제네릭 타입 제한 (Type Constraints)
extends를 사용하여 제네릭 타입에 제한을 걸 수 있습니다.
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
void square() {
print(value * value);
}
}
void main() {
var intBox = NumberBox<int>(10);
intBox.square(); // 100
var doubleBox = NumberBox<double>(2.5);
doubleBox.square(); // 6.25
// var stringBox = NumberBox<String>('오류'); // 컴파일 에러!
}
다중 제네릭 매개변수 & 제네릭 상속
class Pair<K, V> {
K first;
V second;
Pair(this.first, this.second);
}
// 제네릭 클래스를 상속하여 타입 고정
class IntBox extends Box<int> {
IntBox(int value) : super(value);
void increment() {
setValue(getValue() + 1);
}
}
// 타입 별칭
typedef StringList = List<String>;
typedef KeyValueMap<K, V> = Map<K, V>;
컬렉션과 제네릭
List, Map, Set은 모두 제네릭을 활용하여 타입 안전하게 사용할 수 있습니다.
// List
List<String> names = ['홍길동', '김철수', '이영희'];
var fruits = <String>['사과', '바나나', '오렌지'];
var numbers = List<int>.filled(5, 0); // [0, 0, 0, 0, 0]
var evens = List<int>.generate(5, (i) => i * 2); // [0, 2, 4, 6, 8]
var filteredNames = names.where((name) => name.length > 2).toList();
var mappedScores = [90, 85, 95].map((score) => score * 1.1).toList();
// Map
Map<String, int> ages = {
'홍길동': 30,
'김철수': 25,
'이영희': 28,
};
var config = Map<String, dynamic>();
config['debug'] = true;
config['timeout'] = 30;
// Set
Set<String> uniqueNames = {'홍길동', '김철수', '이영희'};
var colors = <String>{'빨강', '파랑', '녹색'};
var nums = Set<int>.from([1, 2, 3, 3, 4]); // {1, 2, 3, 4}
typedef — 타입 별칭 & 함수 타입
복잡한 함수 타입이나 타입에 이름을 부여하여 코드 가독성을 높일 수 있습니다.
// 함수 타입 별칭
typedef IntOperation = int Function(int a, int b);
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
void calculate(IntOperation operation, int x, int y) {
print('결과: ${operation(x, y)}');
}
void main() {
calculate(add, 10, 5); // 결과: 15
calculate(subtract, 10, 5); // 결과: 5
}
// Dart 2.13+ 일반 타입 별칭
typedef StringList = List<String>;
typedef UserInfo = Map<String, dynamic>;
void printNames(StringList names) {
for (var name in names) {
print(name);
}
}
void displayUserInfo(UserInfo user) {
print('이름: ${user[\'name\']}, 나이: ${user[\'age\']}');
}
Dart 3 패턴 매칭과 타입
Dart 3에서는 switch 표현식에서 타입 기반 패턴 매칭이 가능합니다.
Object value = '문자열';
switch (value) {
case String():
print('문자열: $value');
case int():
print('정수: $value');
default:
print('기타 타입: $value');
}
실전 예제: 제네릭 캐시 클래스
class Cache<T> {
final Map<String, T> _cache = {};
T? get(String key) => _cache[key];
void set(String key, T value) {
_cache[key] = value;
}
bool has(String key) => _cache.containsKey(key);
void remove(String key) => _cache.remove(key);
void clear() => _cache.clear();
}
void main() {
var stringCache = Cache<String>();
stringCache.set('greeting', '안녕하세요');
print(stringCache.get('greeting')); // '안녕하세요'
var userCache = Cache<Map<String, dynamic>>();
userCache.set('user1', {'name': '홍길동', 'age': 30});
var user = userCache.get('user1');
print('사용자: ${user?[\'name\']}, 나이: ${user?[\'age\']}');
}
실전 예제: Result 타입 패턴
제네릭을 활용하여 성공/실패를 타입 안전하게 처리하는 Result 패턴입니다.
abstract class Result<S, E> {
factory Result.success(S value) = Success<S, E>;
factory Result.failure(E error) = Failure<S, E>;
bool get isSuccess;
bool get isFailure;
S? get value;
E? get error;
void when({
required void Function(S value) success,
required void Function(E error) failure,
});
}
class Success<S, E> extends Result<S, E> {
final S _value;
Success(this._value);
@override bool get isSuccess => true;
@override bool get isFailure => false;
@override S get value => _value;
@override E? get error => null;
@override
void when({
required void Function(S value) success,
required void Function(E error) failure,
}) => success(_value);
}
class Failure<S, E> extends Result<S, E> {
final E _error;
Failure(this._error);
@override bool get isSuccess => false;
@override bool get isFailure => true;
@override S? get value => null;
@override E get error => _error;
@override
void when({
required void Function(S value) success,
required void Function(E error) failure,
}) => failure(_error);
}
사용법:
Result<String, Exception> fetchData() {
try {
return Result.success('데이터');
} catch (e) {
return Result.failure(Exception('오류: $e'));
}
}
void main() {
var result = fetchData();
result.when(
success: (data) => print('성공: $data'),
failure: (error) => print('실패: $error'),
);
}
참고: fpdart 패키지
더 풍부한 함수형 프로그래밍 기능이 필요하다면 fpdart 패키지를 참고하세요. Either, Option 등 강력한 타입을 제공합니다.
타입 시스템 비교 표
| 키워드 | 타입 검사 | 용도 |
|---|---|---|
| var | 컴파일 타임 추론 | 타입 추론 (변경 불가) |
| dynamic | 없음 (런타임) | 아무 타입이나 할당 가능 |
| Object | 컴파일 타임 | 모든 타입의 상위 타입 |
| is / is! | 런타임 체크 | 타입 확인 + 프로모션 |
| as | 런타임 캐스트 | 명시적 타입 캐스팅 |
| T extends X | 컴파일 타임 | 제네릭 타입 제한 |
Implementation Steps
Type inference — var infers type from initial value, cannot reassign to different type
is/is! and type promotion — inside if (obj is String), String methods are auto-available
Generics — List<T>, Map<K,V> for type-safe collections, extends for type constraints
typedef — name complex function types, typedef Compare<T> = int Function(T a, T b)
Pros
- ✓ Compile-time type checking prevents runtime errors
- ✓ Type promotion allows direct use after is check without casting
Cons
- ✗ Overusing dynamic breaks type safety