
런타임에 타입을 확인해주는 원리로, 타입을 좁혀주기 위해 사용한다.
typeof value === 'string'
타입스크립트한테 개발자가 타입을 알려주는 기능
타입스크립트 엔진보다 내가 더 타입을 잘 알고 있을 때, 즉 타입 추론에 의지하지 않고 타입을 확실하게 지정해주고 싶을 때 사용한다.
타입스크립트 문법을 사용해서 타입 가드 하는 것
타입 단언을 사용하면 타입스크립트 엔진의 평가보다 타입 단언된 타입을 우선하기 때문에 number 타입의 값을 사용하고 string으로 단언하더라도 에러를 발생시키지 않음
타입 단언은 주로 두 가지 문법으로 사용된다.
<>
형태 :<string>value
실무에서 많이 사용하지 않음(리액트 jsx문법과 충돌)
as
형태 :value as string
실무에서 많이 활용되는 형태
타입 단언은 해당 단언이 틀린 경우에도 에러를 발생시키지 않기 때문에 개발자의 부담이 많이 따르는 방식이다.
따라서 타입 단언은 정말 피치 못할 경우에만 사용하는 것을 권장한다.
그렇다면 타입 단언은 어디에서 사용될까? 대표적인 예시는 DOM 조작이다.
const button = document.getElementById("myButton");
// button.click(); // 'button'은 'null'일 수도 있다고 판단해서 에러를 발생시킴 (HTMLElement | null)
(button as HTMLElement).click(); // 타입 단언으로 에러 방지
타입 단언과 널 아님 보장 연산자는 에러가 런타임에 발생하도록 하기 때문에 개발자의 책임 부담이 크다.
타입 단언 문법은 WEB API를 사용했을 때와 같이 타입을 제대로 추론하지 못하는 경우에 사용하고, 널 아님 보장 연산자는 리액트에서 많이 사용된다.
{
// WEB API를 사용했을 때, 타입을 제대로 추론하지 못하는 경우
const button = document.getElementById("myButton");
(button as HTMLElement).click();
// 옵셔널 체이닝 연산자(자바스크립트)
button?.click();
// 널 아님 보장 연산자(타입스크립트)
button!.click(); //
}
{
// 널 병합 연산자
// 변수나 표현식이 null or undefined일때 대체 값을 제공하는 방법
let foo: string | null = null;
let foo2 = foo ?? "default value"; // null, undefined
let foo3 = foo || "default vlaue2"; // falsy, 0, false, NaN, '', undefined, null
console.log(foo2);
console.log(foo3);
}
https://www.typescriptlang.org/play/
다음과 같이 enum 코드를 컴파일하면 코드량이 많아지는 것을 확인할 수 있다.
무분별한 enum 사용은 코드의 길이를 늘리기 때문에 주의가 필요하다.

const 키워드를 사용하면 컴파일 후에 흔적이 남지 않기 때문에 실무에서는 const 키워드로 enum을 사용하는 편이다.

다른 타입은 const 키워드를 사용할 수 없지만 enum은 사용할 수 있다. → const enum이라는 명칭으로 불림
enum(열거형) : 고정된 값들의 집합을 정의하는 데 사용되는 타입
// 1. 숫자 열거형
// 선언 순서에 따라 0부터 1씩 증가하는 값을 가짐
// 그 내부의 값보다는 키를 활용하는 경우가 더 많다.
// 가독성과 안전성을 높일 수 있는 방법
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
console.log(Direction.Down);
// 1-1. 초기값 설정
// 초기값을 할당해준 속성의 다음 속성은 해당 속성의 +1 값을 가짐
enum Direction2 {
Up, // 0
Down = 10, // 10
Left, // 11
Right, // 12
}
console.log(Direction.Down);
// 열거형 타입을 사용하는 예시
// 함수로 입력하는 방식
// input-text로 입력받는 것과 같음 (주관식 선택지))
function move1(direction) {
switch (direction) {
case "Up":
console.log("위로 한 칸 이동");
break;
case "Down":
console.log("아래로 한 칸 이동");
break;
case "Left":
console.log("왼쪽으로 한 칸 이동");
break;
case "Right":
console.log("오른쪽으로 한 칸 이동");
break;
}
}
move1("up"); // 이렇게 오타/오기가 발생할 여지가 있다.
// enum 타입을 활용한 방식
// 셀렉트 타입으로 콤보박스 만드는 것과 같음 (객관식 선택지)
function move2(direction: Direction) {
switch (direction) {
case Direction.Up:
console.log("위로 한 칸 이동");
break;
case Direction.Down:
console.log("아래로 한 칸 이동");
break;
case Direction.Left:
console.log("왼쪽으로 한 칸 이동");
break;
case Direction.Right:
console.log("오른쪽으로 한 칸 이동");
break;
}
}
move2(Direction.Up);
// 2. 문자열 열거형
enum Gender {
Male = "Male",
Female = "Female",
}
// 타입 별칭과 결합
type User = {
name: string;
gender: Gender;
};
const user: User = {
name: "James",
gender: Gender.Male,
};
// enum을 적극 활용하는 조직이 있고, 리터럴 타입을 활용하는 조직이 있다.
// 분위기에 따라 다름
}
{
// 이넘도 인터페이스처럼 선언 병합이 이루어짐
// => 1. 초기값을 지정하지 않는 경우 모든 값이 0을 가지므로 선언 병합이 이뤄지지 않음
enum Direction {
Up, // 0
}
enum Direction {
Down, // 0
}
// => 2. 서로 다른 값임을 확인할 수 있는 경우 병헙이 이뤄짐
enum Direction {
Left = "Left",
}
enum Direction {
Right = "Right",
}
console.log(Direction[0]); // 값을 통해 키를 찾는 역방향 추적이 가능하다.
}
코드의 재사용성을 증가시키기 위해 사용하는 것, 치환(값을 대체하는 것)
타입 별칭, 인터페이스, 함수, 클래스에서 사용할 수 있다.
특정 타입의 값이 여러 개일 때 유용
=> name은 무조건 string일 것이므로 이런 필드에 제네릭을 사용할 필요가 없다.
{
// 제네릭
// 코드의 재사용성을 증가시키기 위해 사용하는 것, 치환(값을 대체하는 것)
// 특정 타입의 값이 여러 개일 때 유용
// => name은 무조건 string일 것이므로 이런 필드에 제네릭을 사용할 필요가 없다.
// 인터페이스 + 제네릭
interface User<T> {
name: string;
value: T;
}
const user: User<"option"> = {
name: "John",
value: "option",
};
// 타입별칭 + 제네릭
type Car<T> = {
name: string;
option: T;
};
const car1: Car<null> = {
name: "benz",
option: null,
};
const car2: Car<string> = {
name: "benz",
option: "key",
};
const car3: Car<{ keyholder: boolean }> = {
name: "benz",
option: {
keyholder: true,
},
};
const car4: Car<boolean> = {
name: "benz",
option: true,
};
{
// 함수 + 제네릭
// 1. 기본 타입 지정
function getFirstElement(arr: number[] | string[]): number | string {
return arr[0];
}
console.log(getFirstElement([1, 2, 3]));
console.log(getFirstElement(["1", "2", "3"]));
// 2. 제네릭
function getFirstElement2<T>(arr: T[]): T {
return arr[0];
}
console.log(getFirstElement2([1, 2, 3])); // 타입을 명시하지 않아도 타입 추론에 의해 에러가 발생하지 않지만
console.log(getFirstElement2(["1", "2", "3"]));
console.log(getFirstElement2<boolean>([true, false])); // 조직 내 룰에 따라 타입을 지정해서 넘기는 경우도 있음
}
{
// 인터페이스 + 제네릭으로 함수 타입 지정
interface FirstFunc {
<T>(arr: T[]): T;
}
const getFirstElement: FirstFunc = (arr) => arr[0];
}
{
interface Pair<T, U, K> {
first: T;
second: U;
third: K;
}
// 리터럴 타입을 지정해주는 방법
const pair: Pair<"A", "B", "C"> = {
first: "A", // 이렇게 작성했을 때는 오류가 안 나지만
// first : "Aㅇㅇㅇ" // 이렇게 하면 오류가 남
second: "B",
third: "C",
};
}
코드의 재사용성을 증가시키기 위해 제네릭을 사용하지만, 너무나 폭 넓은 재사용성을 가지게 해주기 때문에 문제가 되는 경우도 있다.
⇒ extends 키워드를 사용해서 제네릭 제약을 이용하면 특정 속성만 받거나, 특정 속성만 제외하는 등의 범위 조정이 가능하다.
{
// boolean -> []
// T extends U ? X : Y
function getFirstElement4<T extends number | string>(arr: T[]): T {
// 제네릭 제약 문법 : 제네릭 T에 number과 string 타입만 받도록 제약을 걸 수 있음
return arr[0];
}
console.log(getFirstElement4([true, false]));
강의 중 extends로 제약하면 코드가 간결해지는 장점이 사라져서 유니온 타입이랑 크게 차이가 없지 않나요? 라는 질문이 있었다.
제네릭을 사용하지 않는 경우 유니온 타입(|)을 사용해 타입을 하나하나 나열해야 한다.
유니온 타입(|)을 사용할 경우
1. 타입을 하나 하나 나열해야 한다.
2. 동작 중 타입이 추가되는 경우 동적으로 추가되지 않기 때문에 작성한 코드를 따라가 수동으로 바꿔줘야 한다.
제네릭 제약 방식을 사용할 경우
1. extends로 제약 조건을 나열하는 경우 유니온 타입보다 가독성이 더 나빠보일 수 있다.
2. 타입이 추가되는 경우에도 코드를 수정할 필요가 없다.
// 1. 유니온 타입 방식
interface UserBasic {
name: string;
age: number;
}
interface UserWithGender extends UserBasic {
gender: string;
}
interface UserWithAddress extends UserBasic {
address: string;
}
// 유니온 타입으로 정의
type UserInfo = UserBasic | UserWithGender | UserWithAddress;
function printUserInfoUnion(user: UserInfo) {
console.log(`${user.name}, ${user.age}`);
}
// 2. 제네릭 제약 방식
function printUserInfoGeneric<T extends { name: string; age: number }>(user: T) {
console.log(`${user.name}, ${user.age}`);
}
// 사용 예시와 차이점 비교
const user1 = { name: "james", age: 10 };
const user2 = { name: "james", age: 10, gender: "male" };
const user3 = { name: "james", age: 10, address: "seoul" };
const user4 = { name: "james", age: 10, gender: "male", address: "seoul" };
// 유니온 타입 방식
printUserInfoUnion(user1); // OK
printUserInfoUnion(user2); // OK
printUserInfoUnion(user3); // OK
printUserInfoUnion(user4); // Error - 모든 속성이 있는 타입은 정의되지 않음
// 제네릭 제약 방식
printUserInfoGeneric(user1); // OK
printUserInfoGeneric(user2); // OK
printUserInfoGeneric(user3); // OK
printUserInfoGeneric(user4); // OK - 추가 속성 허용
결론적으로 이렇게 정리할 수 있다.
keyof : 객체의 키를 추출해서 유니온 타입으로 바꿔주는 키워드
{
// 객체의 속성 값을 반환하는 함수
function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
// K extends "name"
console.log(getProperty({ name: "james" }, "name"));
// K extends "name" | "age"
console.log(getProperty({ name: "james", age: 20 }, "name"));
// K extends "name" | "age" | "gender"
console.log(getProperty({ name: "james", age: 20, gender: "male" }, "name"));
}
예제와 같이, 제네릭 T는 object 타입만 받도록 제약하고, K는 객체의 키를 추출해서 유니온 타입으로 바꿔준다. (사실상 T를 객체로 제한)
이 부분을 유니온 타입으로 풀어서 쓰면 다음과 같다.
getProperty({ name: "james" }, "name") // 첫 번째 케이스
getProperty({ name: "james", age: 20 }, "name") // 두 번째 케이스
차이점 비교
- 제네릭 버전은 하나의 함수로 모든 케이스를 처리할 수 있다.
- 유니온 타입 버전은 각 케이스마다 별도의 함수가 필요하다.
⇒ 따라서 제네릭 버전이 더 유연하고 재사용 가능한 코드를 만들 수 있다.
클래스를 자바스크립트 코드와 타입스크립트 코드로 각각 작성했을 때의 차이는 다음과 같다.
// 1. 자바스크립트
class Car {
constructor(speed) {
this.speed = speed;
}
getSpeed() {
return this.speed;
}
}
const car = new Car(100);
console.log(car.speed);
console.log(car.getSpeed());
// 2. 타입스크립트
// - 타입스크립트는 속성의 타입까지 명시해줘야 에러가 발생하지 않는다
class Car2 {
speed: number;
constructor(speed: number) {
this.speed = speed;
}
getSpeed() {
return this.speed;
}
}
타입스크립트의 클래스(Class)
- constructor의 매개변수를 지정해주면 this.firstName = firstName 같은 자바스크립트 코드로 자동 변환해준다.
- private 키워드: 클래스 바깥에서 프로퍼티나 메서드에 접근할 수 없게 하는 키워드. 상속받은 클래스에서도 접근할 수 없다.(자바스크립트에서는 작동x)
- protected 키워드: 자식 클래스에서는 프로퍼티나 메서드에 접근할 수 있게 하고, 외부에서는 접근할 수 없도록 하는 키워드.
- 추상 클래스: 다른 클래스가 상속 받을 수는 있지만 새로운 인스턴스는 만들 수 없는 클래스
- 추상 메서드: 추상 클래스 안에 만들 수 있는 메서드. 추상 클래스를 상속 받는 모든 것들이 구현 해야하는 메서드를 의미한다. 메서드를 구현해서는 안되고 call signature만 작성해야한다.
접근 제어자 : public, protected, private
접근 제어자를 지정하지 않으면 기본값은 public이다.
(1) public : 모두 다 접근 가능
(2) private : 해당 클래스에서만 접근 가능
(3) protected : 해당 클래스와 상속 클래스에서 접근 가능
📌 접근 가능한 위치
| 구분 | 선언한 클래스 내 | 상속받은 클래스 내 | 인스턴스 |
|---|---|---|---|
| private | ⭕ | ❌ | ❌ |
| protected | ⭕ | ⭕ | ❌ |
| public | ⭕ | ⭕ | ⭕ |
private를 사용하면 상속받은 클래스 안에서 마저도 this 사용해 접근 불가능
그래서 protected를 사용하면 상속받은 클래스 안에서 this 사용해 접근 가능
물론 protected로 지정된 것들은 외부에서 사용이 불가능
추상 클래스란 기본 구조/골격을 미리 정해놓는 클래스를 말한다.
클래스 선언 앞에 abstract를 붙여 추상 클래스 선언하며, abstract 키워드로 선언된 클래스는 인스턴스를 생성할 수 없다.
{
abstract class CarAbstract {
abstract name: string;
abstract color: string;
abstract start(): void;
// 메소드를 미리 구현해 둘 수 있음
printInfo() {
console.log(`${this.name}, ${this.color}`);
}
}
// 추상 클래스에서 정의하고 있는 것은 무조건 구현 해야 한다.
class Car extends CarAbstract {
name: string;
color: string;
constructor(name: string, color: string) {
super();
this.name = name;
this.color = color;
}
start(): void {
console.log("start!");
}
}
const car = new Car("benz", "black");
인터페이스도 공장 역할을 할 인터페이스를 정의하고, 그것을 상속시킬 수 있다.
하지만 추상 클래스처럼 구현부를 미리 만들어놓을 수는 없다.
추상 클래스는 한번에 여러 개를 상속할 수 없다.
=> extends 키워드는 콤마로 한번에 여러 개의 인스턴스를 생성할 수 없다.
=> 하지만 implements를 사용해서 상속하는 경우 한번에 여러 개의 인스턴스를 생성할 수 있다.
interface Moveable {
move(): void;
}
class Car8 implements Moveable {
move() {
console.log("움직임");
}
}
}
{
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue() {
return this.value;
}
}
const box1 = new Box(10);
console.log(box1.getValue());
class StringBox extends Box<number, number> {
constructor(value: number, value2: number) {
super(value, value2);
}
printString() {
console.log("string box");
}
}
}
오늘 타입스크립트 성취도 평가 테스트가 있었는데 한 문제를 틀렸다.
프로그래머스는 오답 문항을 공개해주지 않아서 동기들이랑 헷갈렸던 문제 맞춰보다가 아무래도 타입 단언 키워드가 언제 동작하는지 묻는 문제를 틀린 것 같아 다시 한번 개념 정리를 해보았다.
✅ 타입 가드 키워드 typeof = 자바스크립트 예약어
타입 단언 키워드<>,as= 타입스크립트 예약어
타입스크립트는 무조건 자바스크립트로 컴파일을 거친 다음에 변환된 자바스크립트가 런타임에 실행되는 거니까 타입 단언은 컴파일에, 타입 가드는 런타임에 동작한다.
⇒ 이것이 바로 타입 가드가 더 안전한 이유
실제로 TypeScript 코드는 JavaScript로 컴파일될 때 모든 타입 정보가 제거된다. 이를 "타입 이레이저(Type Erasure)"라고 한다.
제목에는 주요 키워드만 골라 적었지만 오늘 배운 내용이 많아 TIL을 정리하는 데도 엄청 오래 걸렸다. 다들 바로 문제를 풀던데 어떻게 문제를 그리들 빨리 푸는거지...?
오늘 모딥다 스터디 하고 코테도 풀고 연습문제도 풀어야 하고 플젝하고 타입스크립트 챌린지 문제도 제출해야 하는데... 과연 오늘은 몇 시에 잘 수 있을까
+ 그리고 강사님이 슬랙에 보이면 같이 공부도 하고 질문도 받아 준다고 하셨는데 어떻게 할지 고민중이다. 다같이 허들로 타입스크립트 문제를 풀자고 해볼까?
모이렇게 공부를 많이해요?