TS의 Type 시스템은 다음과 같이 구성된다.
명시적 정의는 변수의 타입을 변수를 생성할 때 정의하는 것이다.
let a : boolean = false
let b : number = 26
보다시피 boolean
과 number
라는 타입을 정의함으로써 그에 맞는 값(false, 26
)을 할당했다.
만약, 각 타입에 맞지 않는 값을 선언하면 어떻게 될까?
let a : boolean = "How's it work?"
→ string
타입을 boolean
타입에 할당할 수 없다는 에러가 발생한다.
여기서 타입 확인은 TS의 Type Checker가 수행한다.
타입을 정의하지 않고 변수만 생성하는 방식도 있다.
let a = false
let b = 26
우리가 타입을 정의하지 않는다면 TS 내부적으로 해당 변수의 타입을 추론한다.
즉, 여기서 a는 boolean, b는 number로 내부적으로 타입을 추론하는 것이다.
이때 값을 기존 타입과 다르게 변경하면 어떻게 될까?
a = 1
Type 'number' is not assignable to type 'boolean'.
라는 오류가 발생한다.
위에서 말했듯이 number
타입을 boolean
타입에 할당할 수 없다는 것이다.
primitive 타입은 원시적 타입이다. number
, string
, boolean
이 이에 속한다.
타입 선언은 위에서 봤듯이 굉장히 단순하다. Java
나 C
를 배웠던 사람이면 익숙할 것이다.
let a : number = 1
let b : string = "s"
let b : boolean = false
숫자 : number
문자열 : string
논리 : boolean
예를 들어, player
라는 object
가 name
프로퍼티를 갖고있다고 가정하자.
const player = {
name: "lunarmoon",
}
이때 player
들 중 몇몇은 age
프로퍼티가 있을수도 있고 없을수도 있고 name
은 항상 가진다고 가정하자.
일단, name
프로퍼티를 바꿔보자.
const player : object = {
name: "lunarmoon",
}
player.name = "john"
위와 같이 object
라는 타입을 명시해서 작성하게되면 object
타입에 name
프로퍼티가 없다고 뜰 것이다.
자명하게도 내부적으로 object
타입에는 name
이라는 프로퍼티는 존재하지 않는다!
(✅ name
은 player
라는 object
에 존재하는 것이다.)
따라서, 우리는 이를 아래와 같이 바꿔야 한다.
const player : {
name : string,
age : number
} = {
name: "lunarmoon",
}
위에서 몇몇의 player
는 age
를 가지지 않을 수 있다고 말했다. 그래서 위와 같이 age의 타입은 명시해주고 값은 전해주지 않았는데 오류가 발생한다.
TS에서 특정 프로퍼티가 값을 가지지 않을수도 있게 하고 싶으면, 단순히 값을 전해주지 않기만 하면 되는게 아니다.
다음과 같이 ?
를 사용해서 표현해야 한다.
const player : {
name : string,
age ?: number
} = {
name: "lunarmoon",
}
이렇게 되면 age
의 타입은 number | undefined
로 표시가 된다.
(아마 Kotlin을 배웠으면 ?
가 익숙할 수 있다. ?.
를 자주 사용하기 때문에..)
자 여기서 if문
을 사용해서 player
의 age
를 조작하고 싶다.
if (player.age < 10) { ... }
이는 틀린 코드이다. 왜 일까? 🧐
앞서 ?:
로 타입을 선언하게 되면 undefined
타입도 같이 할당된다고 했다.
undefined
는 비교 자체를 할 수가 없기 때문에,
먼저 해당 타입이 존재하는지 안하는지 확인해야 한다.
그래서, 아래와 같이 작성해야한다.
if (player.age && player.age < 10) { ... }
자 만약에 같은 구조의 object
인데 변수의 이름만 다르게 하고 싶을 땐 어떻게 할 수 있을까?
단순하게 변수 이름만 다른 똑같은 object를 하나 만들면 된다.
const player2 : {
name: string,
age ?: number,
} = {
name: "john",
age : 18
}
이렇게 만들면 너무 비효율적이다. 새로운 object
를 만들고자 할 때 마다 똑같은 프로퍼티를 또 작성해야 한다. 손목만 아플 뿐..
이때 사용하는 것이 Alias
타입, 즉 별칭 타입이다.
이는 프로퍼티들을 하나의 타입으로 뽑아내는 방식이다.
다음과 같이 만들 수 있다.
type Player = {
name : string,
age ?: number,
}
const player : Player = {
name : "lunarmoon"
}
const player2 : Player = {
name : "john"
age: 18
}
확실히 동일한 코드를 하나의 별칭 타입으로 빼놓으니, 코드가 간결해보인다.
이렇게 되면 코드가 간결해질 뿐만 아니라 코드의 재사용성이 높아진다.
추가로 이 별칭 타입은 object
만 유효한게 아니다. 어느 타입에서든 적용할 수 있다.
여기서는 name
과 age
에 별칭 타입을 적용해보자.
type Age = number
type Name = string
그러면 Player
타입을 다음과 같이 바꿀 수 있다.
type Player = {
name : Name,
age : Age
}
이렇게 별칭 타입을 적용하면, 해당 프로퍼티가 어떤 의미를 갖는지 한눈에 보기 좋아진다.
이번에는 함수의 return 타입
을 어떻게 정의할 수 있는지 살펴보자.
함수도 변수와 마찬가지로 같은 방식으로 타입을 정의한다.
예를 들어, string
타입인 name
을 인자로 받으면 name
을 가지는, 즉 위에서 만들었던 Player 타입
인 객체를 return
하는 함수를 만들어보자.
function playerMaker(name : string) : Player {
return {
name // == name : name
}
함수를 선언할 때 마지막에 : [Type]
만 붙여주면 된다.
추가로, 화살표 함수는 어떻게 return
타입을 정의할 수 있을까?
const playerMaker = (name : string) : Player => ({name})
위와 같이 작성하면 된다.
아마 JS에서는 다음과 같이 만들었을 것이다.
const playerMaker = (name : string) => ({name})
보다시피 : [Type]
만 추가된것을 알 수 있다.
Tuple
은 array
를 생성할 수 있게 하고 정해진 길이(개수)와 순서가 존재하며 특정 위치에 특정 타입이 존재해야 한다.
Tuple
을 한번 만들어보자.
const player : [string, number, boolean] = []
여기서 타입만 선언하고 값을 넣지는 않았는데, 이렇게 되면 에러가 발생한다.
따라서, 값을 꼭 할당해줘야 한다.
const player : [string, number, boolean] = ["lunarmoon", 26, true]
다음과 같이 []
안에 길이(개수)를 정해줘야 한다.
이때, Tuple
은 서로 다른 타입들이 하나의 배열에 존재할 수 있다.
Tuple
을 왜, 언제 써야하는지 와닿지 않을 수 있다.
그냥 Tuple
을 사용하면 항상 정해진 개수의 요소를 가져야 하는 array
를 생성할 수 있다는 것만 기억해두자.
추가로, 특정 위치를 접근하는 것을 같이 살펴보자.
player[0] = 1
해당 접근은 에러가 발생한다. 왜냐하면, player[0]
은 string 타입
인데 number
값을 할당하려고 했기 때문이다. 그리고 readonly
속성도 Tuple
에 추가할 수 있다.
다음 타입들은 JS에도 존재하는 타입들이다.
any
는 말 그대로 anything을 의미한다. any
는 TS로부터 빠져나오고 싶을 때 쓰는 타입이다.
즉, TS의 보호장치들로부터 빠져나오고 싶을 때 사용하는 타입이다.
우리가 아무런 타입을 선언하지 않는다면 기본적으로 any 타입을 갖게된다.
let a = []
위의 a라는 배열은 any 타입을 갖게 된다.
undefined
는 선언도 하지 않고 할당도 하지 않는 타입이다.
말 그대로 정의되지 않음을 의미한다.
null
은 선언은 하되 할당을 하지 않는 타입이다.
any
: 아무 타입
undefined
: 선언 ❌ 할당 ❌
null
: 선언 ⭕️ 할당 ❌
void
는 아무것도 return하지 않는 함수를 대상으로 사용한다.
함수 내에 return 문
이 없다면 TS가 내부적으로 유추한다.
function say() {
console.log("HI!")
}
// "HI!", say() : void
만약 우리가 API로부터 응답을 받는데, 그 응답의 타입이 뭔지 모른다고 가정해보자.
이때 사용하는 것이 unknown
타입이다.
unknown
으로 변수를 선언하고 해당 변수를 가지고 어떤 작업을 하려고 한다면, TS는 이 변수의 타입을 먼저 확인해야 하는 방식으로 일종의 보호장치를 제공한다.
아래의 코드를 살펴보자.
let a : unknown
let b = a + 1
해당 코드는 a is of type 'unknown'
이라는 에러가 발생한다.
따라서 우리는 아래와 같이 먼저 해당 변수의 타입을 먼저 확인해야 한다.
if (typeof a === 'number') {
let b = a + 1
}
if (typeof a === 'string') {
let b = a.toUpperCase()
}
✅ 결론적으로, unknown
은 변수의 타입을 미리 알지 못할 때 사용한다.
never
는 많이 사용하지 않지만 알아두면 좋다.
never
는 절대 실행되지 않음을 의미한다.
아래 코드는 "HI!"
를 return
하는 never 타입
의 함수이다.
function say() : never {
return "HI!"
}
위 코드는 'string을 never에 할당할 수 없다'
는 에러를 발생시킨다.
never
는 함수가 예외를 throw
하거나 프로그램 실행을 종료할 때
, 그리고 절대 return 하지 않을 때
사용한다.
function say() : never {
throw new Error("Error")
}
readonly
는 말 그대로 읽기 전용을 의미한다.
JS에서는 기본적으로 이런 동작이 없는데, TS에서는 지원한다.
자, 위에서 만들었던 Player
의 name
프로퍼티에 readonly
속성을 추가해보자.
그리고 name
의 값을 변경해보자.
type Player = {
readonly name : Name,
age : Age
}
const player : Player = {
name: "lunarmoon",
age: 26
}
player.name = "john"
위 코드에서 name
에 값을 할당할 수 없다는 에러가 뜬다.
즉, readonly
는 '읽기 전용'을 의미하고 한번 값을 할당하면 값을 변경하지 못하게 하는 기능을 한다.
배열에도 한번 적용해보자.
const numbers : readonly number[] = [1,2,3,4,5,6,7]
numbers.push(1)
당연하게도 push할 수 없다는 에러가 뜬다.