자바스크립트와 타입스크립트는 별개의 언어가 아니다. 기존의 문법을 그대로 사용하면서 확장된 문법을 사용하는 언어이다. 그러므로 타입스크립트는 자바스크립트의 "슈퍼셋"이라고 불린다. 그러므로 확장자가 ts 파일에서도 기존 자바스크립트의 문법은 정상적으로 작동하지만, js 파일에서는 ts에서 지원하는 문법을 사용할 수 없다.
그렇다면 왜 우리는 타입스크립트에 그렇게 열광하는 것일까?
타입 스크립트는 정적 타입의 언어이다.
Static typing is when the compiler enforces that values use the same type.
즉 타입스크립트에서는 미리 정의한 타입의 변수만 들어올 수 있도록 만든 것이다.
원래 자바스크립트는 동적 타입의 언어인데, 왜 정적 타입이 선호되는 걸까?
예를 들어 다음 코드를 봐보자..
const add = (a,b) => { return a+b }
const result = add(2,5)
console.log(result) // 7
다음과 같이 우리가 숫자를 더하는 함수에 인자로 숫자를 주면 더한 값을 돌려준다. 하지만 다음 사례에서는 어떨까?
const add = (a,b) => { return a+b }
const result = add(2,'5')
console.log(result) // 25
자바스크립트는 동적으로 타입을 변화하는 특징을 가지고 있기 때문에, 자동으로 숫자를 문자열 형식으로 변화하여 값을 돌려준다.
또다른 사례는 다음과 같다.
const user = {
firstName: "Angela",
lastName: "Davis",
role: "Professor",
};
console.log(user.name); //undefined
일반적인 js 코드에서는 에러가 나지 않고, undefined가 뜬다.
JavaScript는 값이 대입되지 않은 변수 혹은 속성을 사용하려고 하면 undefined 를 반환합니다.
다음과 같은 js의 특징 때문이다. 객체에 없는 속성을 사용하려고 했으므로 에러가 아닌 undefined를 반환한다.
그러한 이유로 추후에 생길 수 있는 에러를 방치하게 될 수 있다. 왜냐하면 다음과 같이 저 코드에서는 오류를 반환하지 않는다. 이런 오류를 반환하지 않고 지속한다면 이후 확인했을 때 큰 일이 이미 벌어지고 난 후일 수 있다.
그리고 프로젝트의 규모가 커지면 어떤 타입이 와야 할지에 대해 직관적으로 알기 힘들다는 점이 있다. 가령 프로젝트에 같이 참여하는 사람들은 이 코드를 직접 적은 사람 빼고는 이해하기가 힘들 것이다.
타입스크립트는 자바스크립트와 마찬가지로 같은 자료형을 사용한다. 하지만 그 타입을 지정해주는데 의미가 있다.
먼저 원시타입에 대해 살펴보자면, 숫자형, 문자형, 불리언 형이 있다.
index.js
let age;
age = `12`
다음과 같이 일반적으로 우리가 js 상에서는 동적으로 타입이 변하기 때문에 아무 이상 없이 잘 쓸 수 있었다.
index.ts
let age : number;
age = `12` // error : Type 'string' is not assignable to type 'number'.
하지만 ts 상에서 숫자형으로 지정해주었지만 문자형으로 선언을 한다면 다음과 같이, 문자형 타입은 숫자형 타입에 할당가능할 수 없다고 나온다. 다른 원시형 자료도 다음과 같이 사용할 수 있다.
index.ts
let userName : string;
userName : 'Max'
let isUser : boolean;
isUser = true;
참조타입에는 배열과 객체가 있다. 원시타입과 참조타입의 차이는 이전글에서 잘 정리해놓았으니 한 번 읽고와도 좋을 것 같다!
배열을 저장할 때는 다음과 같이 들어갈 타입과 빈 배열을 적어준다.
index.ts
let hobbies: string[];
hobbies = ["soccer", "baseball", "volleyball"];
앞에 어떤 자료형이 들어갈지 선언이 되었으므로, 그 이외의 타입은 올 수 없다.
index.ts
let hobbies: string[];
hobbies = ["soccer", "baseball", 10];
//Type 'number' is not assignable to type 'String'.
객체는 안에 데이터의 종류가 변화무쌍하기 때문에, 하나씩 정의해주어야 한다.
index.ts
let person : {
name : string;
age : number;
isMarried : boolean;
}
person = {
name : "Max";
age : 32;
isMarried : true;
}
그러므로 각각의 property에 맞는 자료형이 들어오지 않는다면 이 또한 에러를 내뱉는다.
보통 데이터를 fetching 해올 때 배열 안에 수많은 객체들이 저장된 형태로 받아온다. 이에 저러한 형식을 지키는 객체를 배열로 받아올 수 있는 형태도 존재한다.
index.ts
let people: {
name: string;
age: number;
}[];
people = [
{ name: "Max", age: 32 },
{ name: "Juliette", age: 28 },
];
마지막으로 아무것도 지정하지 않게 되면 any 타입이 지정된다. 이는 어떤 형태도 받을 수 있음을 의미한다. 하지만 처음 선언될 때 값의 자료형이 해당 변수의 자료형으로 고정되므로, 이후 교체할 수 없다.
index.ts
// 타입 지정을 생략하면 any 타입이기 때문에 어떤 타입도 받을 수 있음
let a = 13
// Error : any 타입에 선언된 값의 자료형은 고정됨. 그러므로 type error가 발생
a = "안녕"
이는 타입스크립트 내의 type inference라는 과정을 통해, 자동적으로 타입을 비교할 수 있게 만든다. 그러므로 모든 변수에 자료형을 일일이 지정하기 보다는, 저렇게 선언되는 구조에서는 타입을 지정하지 않아도 된다. 하지만 초기값이 없는 경우엔 당연히 자료형을 지정해야 할 것이다!
한 변수에는 보통 하나의 자료형만 들어가는 것이 맞으나, 어떠한 상황에서는 복수의 자료형을 요청받을 수도 있다. 예를 들어 구글폼을 통해 나이를 받을 때 숫자로 하는 사람이 대부분이지만, 특정 사람은 한글로 나이를 적을 수도 있다. 이에 복수의 타입을 걸어줄 수 있는 Union Type이 존재한다.
사용방법은 쉽다. js에서 단락회로 평가 시 쓰였던 " | " 문자를 사이사이에 넣어주면 된다.
index.ts
let age : string | number = 13;
a = "열아홉살"
원시타입과 객체를 섞어쓰는 것도 가능하다.
index.ts
let userName : string | string[];
이렇게 각각의 변수에 들어갈 자료형을 우리는 Type Inference의 기능을 통해 일일이 작성하지 않아도 된다고 배웠다.
하지만 객체의 경우엔 안의 속성값에 대한 정의가 각각 필요하기 때문에 어쩔 수 없이 같은 타입을 공유하더라도 중복해서 써야만 할 수도 있다.
Type Alias 적용 전
index.ts
let userInfo : {
name : string;
age : number;
};
...
let userInfoList : {
name : string;
age : number;
}[];
이때 중복으로 쓰이는 type을 하나의 Alias로 지정하면 코드의 낭비를 막고 재사용이 가능하다. alias는 별명이라는 뜻으로, 재사용이 되는 자료형에 별명을 붙이고, 이를 다시 꺼내쓰면 된다.
Type Alias 적용 후
index.ts
type Person = {
name : string;
age : number;
}
let userInfo : Person;
let userInfoList : Person[];
Functions Type
우리는 지금까지 자료형에 타입을 지정하고 값을 선언해보았다. 하지만 함수에도 타입을 지정할 수 있다. 하지만 굳이 복수의 값이 필요하지 않다면 지정할 필요가 없다. Type Inference를 통해 알아서 함수의 반환값을 추론해내기 때문이다.
index.ts
// 반환값이 있는 경우, type Inference를 통해 함수의 반환값에 대해서도 자동으로 자료를 지정
const add = (a : number, b : number) => {
return a + b;
}
// 지정할 수도 있으나, 특별한 경우가 아니라면 하지 않아도 됨
const add = (a: number, b: number): number => {
return a + b;
};
function hi(a: number, b: number): number {
return a + b;
}
하지만 반환값이 없는 함수도 존재한다. 이들을 반환값으로 받게 되면 undefined 를 반환하게 된다.
index.ts
타입 미지정 시 (알아서 type Inference)
function print(value : any) {
console.log(value
}
타입 지정 시
function print(value : any) : void {
console.log(value
}
하지만 이런 정적 타입의 스크립트도 피해갈 수 없는 사각지대가 존재하는데 다음과 같은 상황을 봐보자.
index.ts
function insertAtBeginning(array : any[], value : any[]) {
const newArray = [value, ...array];
return newArray
}
const demoArray = [1,2,3];
// 반환값의 type도 any, typescript의 power를 사용할 수 없는 상태
const updatedArray = insertAtBeginning(demoArray, -1);
해당 함수는 매개 변수로 어떤 타입의 변수도 받을 수 있도록 any를 지정해주었다. 하지만 함수를 통해 매개변수를 받을 때에는, 매개 변수에 any 타입을 지정해준 후 할당되는 값에 따라 타입이 정해지지 않는다.
왜냐하면, any 타입으로 받은 매개 변수로 만들어진 반환값은 당연히 any 타입의 반환값을 가져야 하기 때문이다.
any 타입의 매개 변수지만, 반환값은 number로 지정해줄 수도 있긴 하다.
그렇기에 타입을 컨트롤하기 위해 만들었지만, 결과값이 any이면 무슨 소용이 있겠는가?
그러면 만약 숫자만 받도록 미리 손써놓을 수도 있다. 다음과 같이 말이다.
index.ts
function insertAtBeginning(array: any[], value: any): number[] {
const newArray = [value, ...array];
return newArray;
}
const demoArray = [1, 2, 3];
const updatedArray = insertAtBeginning(demoArray, -1);
updatedArray[0].split(''); // error 숫자형만을 return하기 때문
하지만 다음 경우는 문자열이 들어오는 경우에 대해 처리하지 못할 뿐더러, 재사용성이 떨어진다. 오직 숫자형 자료만이 들어있는 배열만을 return 하도록 설계되었기 때문이다. 그러므로 이런 재사용성에 대한 문제를 해결하기 위한 type이 바로 generic 타입이다. 다음과 같이 generic type을 사용하기 위해서는 새로운 타입이 필요하다.
index.ts
function insertAtBeginning<T>(array: T[], value: T) {
const newArray = [value, ...array];
return newArray;
}
const demoArray = [1, 2, 3];
const updatedArray = insertAtBeginning(demoArray, -1);
updatedArray[0].split('');
다음과 같이 괄호를 하나 더 지정하여 T라는 타입을 선언해준다. 저기서는 어떤 형태든 상관이 없으나, Type의 대문자를 딴 T를 관행적으로 쓴다.
이제 타입스크립트에게 우리는 any 타입이 아니라, array와 value가 같은 자료형을 가져야 함을 알려준 것이다. 그러므로 type Inference를 통해 들어온 인자가 모두 숫자형 배열이거나 숫자형이므로, typescript는 자동으로 저 return 값은 숫자형이여야 함을 인식할 수 있다.
그러므로 split 또한 정상작동함을 볼 수 있다.
index.ts
function insertAtBeginning<T>(array: T[], value: T) {
const newArray = [value, ...array];
return newArray;
}
const demoArray = ["a", "b", "c"];
const updatedArray = insertAtBeginning(demoArray, "d");
// ['d'] , split => 문자열을 잘라서 배열로 반환하는 함수
console.log(updatedArray[0].split(''));
물론 제네릭 형태를 명시적으로 정하면 더욱 예외를 최소화시킬 수 있다. 즉, 재사용은 가능하되 typescript가 type inference를 하도록 나두는 것이 아니라, 호출하는 쪽에서 명시적으로 제네릭 타입을 설정하는 경우이다.
function insertAtBeginning<T>(array: T[], value: T) {
const newArray = [value, ...array];
return newArray;
}
const demoArray = ["a", "b", "c"];
// 제네릭 타입을 string으로 고정한 경우
const updatedArray = insertAtBeginning<string>(demoArray, "d");