I have a dataframe that looks somewhat like the following. A1U_sweet is actually the 19th column in the real dataframe, and C1U_sweet is the 39th column in the real dataframe. T
Try using head(types) to see if your types object has the information you intended it to. If not, adding value=TRUE to your grep command may be the solution you're looking for.
types <- grep('^A([0-9]|[12][0-9])[A-Z]_[a-z]+', names(df), value=TRUE)
types <- substr(types, 2, Inf) ## Remove the "A"
for (tp in types) {
aa <- df[[paste0('A', tp)]] ## "A" column
cc <- df[[paste0('C', tp)]] ## "C" column
df[[paste0('B', tp)]] <- ifelse(is.na(aa), aa, cc)
}