How can I report a failure in a dry-types constructor?

You could do this:

# frozen_string_literal: true

require 'dry/validation'
require 'date'

module Types
  include Dry::Types()

  MyDate = Types::Date.constructor do |value|
    ::Date.today + 1 if value.eql?('1 day')
  end
end

class MyContract < Dry::Validation::Contract
  params do
    required(:duration).filter(:string, :filled?).value(Types::MyDate)
  end
end

contract = MyContract.new

puts contract.(duration: nil).inspect
#<Dry::Validation::Result{:duration=>nil} errors={:duration=>["must be a string"]}>

puts contract.(duration: "").inspect
#<Dry::Validation::Result{:duration=>""} errors={:duration=>["must be filled"]}>

puts contract.(duration: "foo bar").inspect
#<Dry::Validation::Result{:duration=>nil} errors={:duration=>["must be a date"]}>

puts contract.(duration: "1 day").inspect
#<Dry::Validation::Result{:duration=>#<Date: 2019-07-31 ((2458696j,0s,0n),+0s,2299161j)>} errors={}>

You can read more about filter rules here.