A linguagem JavaScript é uma linguagem de tipagem dinâmica, isso significa que ela é uma linguagem flexível, a grosso modo, ela tenta fazer por conta própria o melhor que ela pode, para que o nosso programa não “quebre”.
Tipagem dinâmica só quer dizer que uma variável pode mudar de tipo durante o programa. Por exemplo:
// variável recebe um número
var x = 1;
...
// depois, ela pode mudar para uma string
x = 'abc';
Em linguagens com tipagem estática (como por exemplo C#, Java e C) isso não é possível: uma vez que a variável é declarada como sendo de um tipo, vc não pode mais mudá-lo.
Mas isso não tem a ver com esse comportamento do JavaScript. Python, por exemplo, também tem tipagem dinâmica, mas nenhuma das operações é possível:
# Ao contrário do JavaScript, em Python todas essas operações dão erro
# TypeError: unsupported operand type(s) for -: 'str' and 'int'
print("1" - 1)
# TypeError: unsupported operand type(s) for -: 'str' and 'str'
print("1" - "1")
# TypeError: can only concatenate str (not "int") to str
print("1" + 1)
# TypeError: unsupported operand type(s) for -: 'str' and 'str'
print("1" - "A")
Repare que em todos os casos ocorre um TypeError. No primeiro caso, é porque vc não pode subtrair um número de uma string. No segundo e quarto casos, não é possível subtrair duas strings. E no terceiro caso, ele não deixa concatenar uma string e um número.
A diferença entre JavaScript e Python, portanto, não é no estilo de tipagem (ambas possuem tipagem dinâmica). A diferença é que o JS faz type coercion (coerção de tipos): quando os operandos de um operador não são do mesmo tipo, um deles é convertido e só depois é feita a operação. Já em Python optaram por não fazer essas conversões automáticas, e em vez disso ocorre um erro.
As regras de type coercion do JavaScript são muitas, e bem confusas (e na minha opinião, muitas sequer fazem sentido prático). Daí gera esses casos bizarros, tanto que gerou pérolas como essa.
Enfim, existem várias fontes que documentam essas regras, então vou acrescentar mais uma: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators
Ali tem um bom resumo para cada operador, além de sempre ter um link para a especificação da linguagem (com uma explicação bem mais técnica e detalhada).
Sabendo disso, se vc quer fazer operações aritméticas, o ideal é sempre converter para número antes de fazer os cálculos. Foi sugerido usar Number, mas vc também pode usar parseInt ou parseFloat (caso seja números inteiros ou de ponto flutuante, respectivamente).
Apesar de funcionar da mesma forma para a maioria dos casos, existem algumas diferenças. Por exemplo, se tiver uma string vazia, Number converte para zero, e os demais retornam NaN:
console.log(Number('')); // 0
console.log(parseInt('')); // NaN
console.log(parseFloat('')); // NaN
O mesmo ocorre com null. Mas com undefined, todos retornam NaN.
E existem vários outros corner cases:
// parseInt e parseFloat ignoram a parte do texto que não é número
var s = '10 acabates';
console.log(Number(s)); // NaN
console.log(parseInt(s)); // 10
console.log(parseFloat(s)); // 10
// começa com "0x", valor é tratado como hexadecimal, exceto por parseFloat
var s = '0x1f';
console.log(Number(s)); // 31
console.log(parseInt(s)); // 31
console.log(parseFloat(s)); // 0
Além disso, parseInt também permite informar em qual base o valor está:
var s = '101';
// o padrão é base 10
console.log(parseInt(s)); // 101
// em binário
console.log(parseInt(s, 2)); // 5
// base 8
console.log(parseInt(s, 8)); // 65
// hexa
console.log(parseInt(s, 16)); // 257
Enfim, o importante é saber exatamente o que vc precisa, se vai aceitar as conversões automáticas, se tem strings vazias ou valores nulos, e o que fazer em cada caso. E aí escolher a conversão mais adequada antes de fazer os cálculos.