About

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

1

Type inference — var infers type from initial value, cannot reassign to different type

2

is/is! and type promotion — inside if (obj is String), String methods are auto-available

3

Generics — List<T>, Map<K,V> for type-safe collections, extends for type constraints

4

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

Use Cases

Designing type-safe API response model classes
← Dart Basic Syntax: var/final/const and the ?, !, ?? Operators Dart Classes: extends vs implements vs with →