In case it's not clear from the above: Crystal supports type annotations, they are enforced by the compiler, and it's not uncommon to use them in practice.
However, there are some cases where the interface for an object is obvious but hasn't been named formally. For example, consider the following valid Crystal method:
def json_vs_yaml(obj)
puts obj.to_json
puts obj.to_yaml
end
In a more strictly typed language (that doesn't support intersection types), I couldn't do that without first defining some interface that includes `to_json` and `to_yaml` and then maybe convincing the compiler that my objects qualify for the interface (by extending their class definitions or casting at the call site), which is a lot of work to enable a trivial method whose purpose is already clear. In Crystal, the compiler just checks that whatever you're passing into the method has `to_json` and `to_yaml` defined and lets you get on with life.
In more complex cases, I almost always find myself including type annotations in method signatures (which will be enforced at compile time) anyway because it makes writing the method easier. And I certainly won't tell you you're wrong to prefer a language with more stringent requirements, especially if you have to work with other people's code a lot. But I do believe that there is a minority of cases where leaving off the type increases the clarity and conciseness of the code.
Another good case is removing redundancy on wrapper methods:
def foo(x : Int32) : Bool
x == 12
end
def add_and_foo(left, right)
foo(left + right)
end
Here, `foo` is fully specified and doesn't rely on type inference at all. `add_and_foo` automagically works on any types that return an Int32 when you add them. If I later update `foo` to operate on a different numeric type, `add_and_foo` still just does the right thing without needing any edits. Even though in practice `add_and_foo` as written is probably going to operate on other Int32's all the time, conceptually the method's parameters are of type "whatever foo takes" and its return type is "whatever foo returns" and letting the compiler enforce that via type inference communicates this intention more clearly than specifying those types concretely.