모든 프로그래밍 언어는 변수를 선언 하는 것 부터 시작한다. 값을 저장할 수 있는 공간(메모리)을 선언하고, 그 공간에 데이터를 할당한다.
타입을 결정하는 시점에 따라 정적 타입과 동적 타입으로 구분할 수 있다.
모든 변수의 타입이 컴파일타임에 결정된다. (C, Java, Typescript … ) 컴파일타임에 타입 에러를 발견할 수 있기에 안정성을 보장한다.
int c = 4;
// 이렇게 타입을 작성해야하고, 한번 정해진 변수는 다른 타입을 넣을 수 없다.
TS를 사용하는 이유 중 가장 큰 이유가 이에 해당한다. 정적 타이핑을 지원한다는 점.
변수 타입이 런타임에서 결정된다. (Python, JS … ) 개발자는 직접 타입을 정의해줄 필요가 없으며, 프로그램을 실행할 때 타입에러가 발견되기에 개발 과정에서 자유롭게 코드를 작성할 수 있다.
컴파일타임
기계(컴퓨터, 엔진)가 소스코드를 이해할 수 있도록 기계어로 변환되는 시점
런타임
컴파일 이후 변환된 파일이 메모리에 적재되어 실행되는 시점
타입 애너테이션이란, 변수나 상수 혹은 함수의 인자와 반환 값에 타입을 명시적으로 선언해 어떤 타입 값이 저장될 것인지를 컴파일러에 직접 알려주는 문법이다.
int woowahanNum = 2010; // Integer (whole number)
float woowahanFloatNum = 2.01f; // Floating point number
char woowahanLetter = 'B'; // Character
boolean woowahanBool = true; // Boolean String
변수 앞에 데이터 타입을 작성한다.
woowahanText = "WoowaBros";
// 🚨 error: cannot find symbol woowahanText
식별자를 찾지 못했다는 에러가 발생했다. 이처럼 자바에선 항상 변수 이름보다 데이터 타입을 우선 명시해줘야 한다.
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let x: [string, number]; // tuple
변수 이름 뒤에 : type
구문을 붙여 데이터타입을 명시해준다.
일반적으로, 타입을 사용하는 언어에서의 객체는 하나의 구체적인 타입을 가지고 있다. 타입은 이름으로 구분되며 이를 명목적 타이핑이라고 칭한다.
그러나 Typescript는 다르다. 이름으로 타입을 구분하지 않고, 구조로 타입을 구분한다. 이를 구조적 타이핑이라고 한다.
interface Developer {
faceValue: number;
}
interface BankNote {
faceValue: number;
}
let developer: Developer = { faceValue: 52 };
let bankNote: BankNote = { faceValue: 10000 };
developer = bankNote; // ✅ OK
bankNote = developer; // ✅ OK
객체가 가지고 있는 속성(프로퍼티)을 바탕으로 타입을 구분하는 것이다. 이름이 다른 객체라도 가진 속성이 동일하다면 타입스크립트는 서로 호환이 가능한 동일한 타입으로 여긴다.
interface Pet {
name: string
}
interface Cat {
name: string
age: number
}
let pet: Pet;
let cat: Cat = { name: "Zag", age: 2 };
// ✅ OK
pet = cat;
console.log(pet); // { name: 'Zag', age: 2 }
cat객체가 Pet 인터페이스에 정의된 모든 속성(name)을 가지고 있기에 문제없이 수행된다.(…)
이는 함수의 매개변수에도 적용된다.
interface Pet {
name: string
}
let cat = { name: "Zag", age: 2 };
function greet(pet: Pet) {
console.log(`Hello, ${pet.name}`); // Hello, Zag
console.log(pet); // { name: 'Zag', age: 2 }
}
greet(cat); // ✅ OK
이게 구조적 타이핑이다.
근데 interface
에 객체를 넣은 후 매개변수로 전달하지 않고, 직접 객체값을 선언해 대입하면 어떻게 될까?
놀랍게도 에러를 뱉어낸다. 직접 대입하면 안되고 중간에 변수를 거쳐 넣으면 된다. 이 원리가 뭘까?
TS에선 객체가 특정 인터페이스나 타입에 할당될 때, 모든 프로퍼티가 명시적으로 타입에 정의되어있어야 한다. 이를 통해 예기치 않은 프로퍼티로 인한 버그를 방지할 수 있다.
객체 리터럴이 직접 함수의 매개변수로 전달되거나 변수에 할당될 때, 타겟 타입에 정의된 프로퍼티만을 가지고 있는지 검사한다.
타겟 타입(Pet
)에 없는 프로퍼티(age
)가 객체 리터럴에 포함되어 있다면, 에러를 발생시킨다.
더 쉽게 정리해서,
그래서 명확하게 인터페이스를 정의하는 것이 중요하다..
class Cat {
String name;
public void hit() {}
}
class Arrow {
String name;
public void hit() {}
}
public class Main {
public static void main(String[] args) {
// 🚨 error: incompatible types: Cat cannot be converted to Arrow
Arrow cat = new Cat();
// 🚨 error: incompatible types: Arrow cannot be converted to Cat
Cat arrow = new Arrow();
}
}
Cat과 Arrow클래스는 String타입의 name변수와 hit() 메서드를 가지고 있다는 점에서 구조적으론 동일하다. 하지만 명목적 타이핑을 채택한 언어에선, 이름으로 타입을 구분하기 때문에 구조가 같더라도 다른 타입으로 취급한다.
명목적 타이핑이 조금 더 안전하다. 객체의 속성을 다른 객체의 속성과 호환시키지 않기 때문이다.
??? : 아니 그럼 TS는 왜 구조적 타이핑을 채택했어요?
TS가 JS를 모델링한 언어이기 때문이다. JS는 기본적으로 덕 타이핑을 기반으로 한다.
덕 타이핑(duck typing)
어떤 함수의 매개변숫값이 올바르게 주어진다면, 그 값이 어떻게 만들어졌는지 신경쓰지 않고 사용한다.
사람이 오리처럼 행동하면, 오리라고 봐도 무방하다. 타입을 미리 정하는게 아니라, 런타임시에 결정(동적 타입)되기에 그렇다.
TS는 JS의 이러한 특징을 받아들여 객체나 함수가 가진 구조적 특징을 기반으로 타이핑하는 방식을 택했다. 더욱 유연한 타이핑이 가능해졌다.
타입을 검사하는 시점에 차이가 있다.
덕 타이핑 : 런타임에 타입을 검사한다. (동적 타입 JS)
구조적 타이핑 : 컴파일타임에 타입체커가 타입을 검사한다. (정적 타입 TS)
그러나 구조적 타이핑의 특징 때문에, 예상하지 못한 객체를 할당할 수 있기에 이러한 한계를 극복하고자 유니온같은 방법이 생겨났다.