use Types::Common::Numeric qw(PositiveInt); has user_id => ( is => 'ro', isa => PositiveInt, ); #### package MyApp::Types { use Type::Library -base, -declare => qw( UserId UserIdList ); use Type::Utils -all; BEGIN { extends qw( Types::Standard Types::Common::Numeric Types::Common::String ); }; declare UserId, as PositiveInt, where { $_ > 1000 }; declare UserIdList, as ArrayRef[UserId]; ...; } #### declare User, as InstanceOf['MyApp::User']; coerce User, from UserId, via { MyApp::Utils::find_user_by_id($_) }; coerce UserId, from User, via { $_->user_id }; #### package MyApp::Types { use Type::Library -base; use Type::Utils (); # don't import any keywords BEGIN { # Type::Utils is still the easiest way to do this part! Type::Utils::extends(qw( Types::Standard Types::Common::Numeric Types::Common::String )); }; my $userid = __PACKAGE__->add_type({ name => 'UserId', parent => PositiveInt, constraint => '$_ > 1000', }); my $user = __PACKAGE__->add_type({ name => 'User', parent => InstanceOf['MyApp::User'], }); $userid->coercion->add_type_coercions( $user => '$_->user_id' ); $user->coercion->add_type_coercions( $userid => 'MyApp::Utils::find_user_by_id($_)', ); __PACKAGE__->add_type({ name => 'UserIdList', parent => ArrayRef[$userid], coercion => 1, }); ...; __PACKAGE__->make_immutable; } #### # Imagine this is some code in a class... # use MyApp::Types qw( UserId Str ); # This will die because UserId is immutable now. UserId->coercion->add_type_coercions(Str, sub { ... }); # This will work, and only affect this one attribute. has user_id => ( is => 'ro', isa => UserId->plus_coercions(Str, sub { ... }), coerce => 1, );