Other than arrays, strings, structs, enums, and maps, everything else is called elementary types.
If an operator is applied to different types, the compiler tries to implicitly convert one of the operands into the type of the other. In general, an implicit conversion between value-types is possible if it makes sense semantically and no information is lost: uint8 is convertible to uint16 and int128 to int256, but int8 is not convertible to uint256 (because uint256 cannot hold, for example, -1). Furthermore, unsigned integers can be converted into bytes of the same or larger size, but not vice versa. Any type that can be converted into uint160 can also be converted into address.
Solidity also supports explicit conversion. So if the compiler doesn't allow implicit conversion between two data types, then you can go for explicit conversion. It is always recommended that you avoid explicit conversion because it may give you unexpected results.
Let's look at an example of explicit conversion:
uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now
Here we are converting uint32 type to uint16 explicitly, that is, converting a large type to a smaller type; therefore, higher-order bits are cut-off.