자바스크립트는 어떤 함수의 매개변수 값이 모두 제대로 주어진다면, 그 값이 어떻게 만들어졌는지 신경 쓰지 않고 사용한다. 이것을 바로 덕 타이핑 기반이라고 한다.
그래서 타입스크립트는 매개변수 값이 요구사항을 만족한다면, 자바스크립트의 타입을 신경 쓰지 않는 동작을 그대로 모델링한다.
예시를 통하여 이해하여 보자.
interface Vector2D {
x: number;
y: number;
}
이를 계산하는 함수는 아래와 같다.
function calculateLength(v: Vector2D){
return Math.sqrt(v.x * v.x + v.y * v.y);
}
이 상황에서 Vector2D
함수를 수정한다면 어떻게 될까?
interface NamedVector {
name: string;
x: number;
y: number;
}
NamedVector
는 x
와 y
가 존재하기 때문에, 위의 함수를 그대로 이해할 수 있다.
그 이유는 타입스크립트가 자바스크립트의 런타임 동작을 모델링하였기 때문인데, NamedVector
의 구조가 Vector2D
와 호환되기 때문이다.
이 상황을 바로 구조적 타이핑을 사용한다고 이해하면 된다.
하지만 이런 구조적 타이핑 때문에 문제가 발생하기도 한다.
interface Vector3D {
x: number;
y: number;
z: number;
}
벡터의 길이를 1로 만들어주는 함수를 사용하여 보자.
function normalize(v: Vector3D) {
const length = calculateLength(v);
return {
x: v.x / length,
y: v.y / length,
z: v.z / length,
};
}
calculateLength
함수가 애초에 2차원 기반으로 만들어졌으니까, 당연히 원하는 결과를 얻을 수 없다. 그럼 왜 이 문제는 에러가 발생하지 않고 3D백터를 매개변수로 잡아내는 것일까?
구조적 타이핑은 매개변수 값이 요구사항을 만족한다면, 자바스크립트의 타입을 신경 쓰지 않는 동작을 그대로 모델링한다고 했다. calculateLength
입장에서는 x, y가 존재하기 때문에 Vector2D와 호환되는 것이다. 그래서 오류가 발생하지 않은 것이고, 타입 체커가 문제라고 짚지 않은 것이다.
그래서, 타입스크립트의 타입은 항상 열려있다. 매개변수의 속성이 매개변수에 타입에 선언된 속성만을 가질 것이라고 생각하면, 봉인된 타입이라고 생각하는 것인데, 이러한 생각 때문에 실수를 하게 되는 것이다.
function calculateLength1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
const coord = v[axis];
//'string'은 'Vector3D'의 인덱스로 사용할 수 없기에 엘리먼트는 임시적으로 '`any'타입입니다.
length += Math.abs(coord);
}
return length;
}
우리는 Vector3D의 프로퍼티에 대한 속성을 number
로 지정해뒀다. 그런대 왜 이런 요상한 오류가 뜨는 것일까?
그 이유는 바로 아래와 같이 작성하게 될 수 있기 때문이다.
const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D); //정상.. NaN을 반환한다.
함수는 오픈된 친구이기 때문에, axis의 타입이 string이 될수도 있다. 그래서 타입스크립트는 v[axis]
의 타입을 Number
라고 확정할 수 없는 것이다. 그래서 정확한 타입으로 객체를 루프 돌리는 코드는 어렵다.
이왕이면 아래와 같이 작성하는 것이 깔끔하며 예기치 못한 오류를 줄이는 방법이 되겠다.
function calculateLengthL1(v: Vector3D){
return Math.abs(v.s) + Math.abs(v.y) + Math.abs(v.z);
}
interface Author {
first: string;
last: string;
}
function getAuthors(database: PostgresDB): Author[] {
const authorRows = database.runQuery('SELECT FIRST, LAST FROM AUTHORS');
return authorRows.map(row => ({first: row[0], last: row[1]}));
이 상태에서 원래는 PostgresDB
를 생성해야 한다. 하지만 아래와 같이 작성한다면 더욱 편리하게 테스팅을 진행할 수 있다.
interface DB {
runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery('SELECT FIRST, LAST FROM AUTHORS');
return authorRows.map(row => ({first: row[0], last: row[1]}));
이렇게 작성하게 되면 실제 구조적으로 유사한 postgresDB
도 getAuthors
함수를 사용할 수 있고, DB
도 사용이 가능하다. 해당 인터페이스만 충족해도 괜찮기 때문이다.
추상화를 통하여 특정한 구현으로부터 분리할 수 있다는 점이 유용하게 사용할 수 있다.
저번 프로젝트 때 any 남발했다가 스스로 죄책감 느끼고 후회하며 코드를 많이 고쳤기 때문에 그 기억을 되살리면서 이번 item을 읽었더니 더욱 재미있었다. ㅋ.ㅋ
타입스크립트의 타입 시스템은 점진적이고, 선택적이다.
이 말을 풀어 쓰자면 코드에 타입을 조금씩 추가할 수 있기 때문에 점진적이고, 언제든지 타입 체커를 해제할수 있기 때문에 선택적이다. 이 기능은 any
때문에 설명하게 된 것이라고 할 수 있다.
내가 처음에 any를 썼던 것도, 어떤 타입이 들어가는지 잘 모르겠어서, 그리고 귀찮아서 (..) 그랬었다.
부득이하게 any를 사용한다고 해도 어떤 위험성이 존재하는지 잘 알고 쓰는게 맞지 않겠는가..!!
let age: number;
age = '12'
any를 사용하게 되면 해당 코드도 에러 없이 잘 수행된다.
직관적으로 age에는 number
타입을 사용하는 것이 맞다. 그러나 이러한 상황에는 string
을 대입해도 에러가 생기지 않아서 혼돈을 야기한다.
함수를 작성할 때에는 시그니처를 명시하고 꼭 지켜야 한다.
any를 사용하면 이 약속을 어기게 된다. 오류 없이 실행된다고 해도 다른 곳에서 문제를 일으킬 수 있으므로 주의 깊게 생각하고 사용하자.
타입스크립트 언어는 자동완성 기능을 제공해주는데, any를 사용하면 아무 도움을 받지 못한다.
그리고 타입 포맷팅과 이름을 수정해주는 기능도 이용할 수 없다.
interface ComponentProps {
onSelectItem: (item: any) => void;
}
콜백이 있는 컴포넌트일 때,
function renderSelector(props: ComponentProps) {/*...*/}
let selectedId: number = 0;
function handleSelectedItem(item: any){
selectedId = item.id;
}
renderSelector({onSelectItem: handleSelectItem});
여기서, onSelectItem
에 아이템 객체를 필요한 부분만 전달하는 , 즉 id만 전달하게끔 컴포넌트를 개선해보자.
interface ComopnentProps {
onSelectItem: (id:number) => void;
}
이렇게 개선을 하였는데, handleSelectedItem
은 any
를 받아서 id를 전달받아도 문제가 없다고 나온다. 하지만, 타입 체커를 통과를 하기는 하지만, 런타임에는 오류가 발생하게 되는 상황이 발생할 수 있다.
만약, item을 any로 미리 설정해뒀다면, 타입 체커가 컴파일 시간에 사전에 미리 에러를 발견했을 것이다!
애플리케이션 상태 같은 객체를 정의하는 것은 매우 복잡하다.
상태 객체 안의 타입을 일일이 작성할 때 any를 사용하면 안된다.
특히, 객체를 정의할 때에는 상태 객체의 설계를 감춰버리게 되므로, 협업시에 매우 큰 어려움이 생기며 동료가 코드를 이해하고 설계를 이해하는 데 어렵게 만들어버린다.
사람은 실수를 하기 때문에 타입 체커가 실수를 잡아준다.
하지만 any를 사용하게 되면 타입 체커도 타입 오류를 잡지 못하여서 신뢰할 수 없는 상황이 되어버린다.
그렇기 때문에 any타입 사용을 지양하여 타입에 발견될 오류를 미리 잡아서 신뢰도를 높일 수 있다.