Post

타입스크립트 (TypeScript)

타입스크립트(TypeScript)에 대해 정리한 페이지입니다.

타입스크립트 (TypeScript)

Tags
TypeScript

개요

타입스크립트(TypeScript)에 대해 정리한 페이지입니다.

타입스크립트란?

타입스크립트(TypeScript)란 자바스크립트에 타입을 추가한 언어입니다. 타입스크립트는 타입을 지정하지 않고 인터프리터가 타입을 유추하는 동적 타입 언어인 자바스크립트와 달리, 타입이 같지 않다면 컴파일 에러를 내는 강 타입 언어입니다. 확장자로는 .ts를 사용하며, 자바스크립트로 컴파일(트랜스파일)되는 언어입니다.

Info.
동적 타입: 자바스크립트처럼 타입을 지정하지 않고 인터프리터가 타입을 유추하는 언어로, 런타임 시 타입이 결정됩니다.
정적 타입: 컴파일 시 타입이 결정되는 언어

약 타입: 비슷한 타입 간에는 자동으로 변환하는 언어 (Ex. Java - float 형 변수에 int 변수를 할당하는 경우 컴파일 에러가 나지 않고, 컴파일러가 내부적으로 float로 변경)
강 타입: 타입이 같지 않다면 컴파일 에러를 내는 언어

타입스크립트의 이점

타입스크립트를 사용하면 다음과 같은 이점이 있습니다.

  • 컴파일 시점에 에러 확인

    타입스크립트를 사용하면 런타임 시점 이전인 컴파일 시점에 에러를 확인할 수 있습니다.

    예를 들어 다음과 같이 두 수의 합을 반환하는 자바스크립트 코드가 있다고 가정합니다.

    1
    2
    3
    4
    5
    
    function add(a, b) {
      return a + b;
    }
    
    console.log(add(10, "20")); // "1020"
    

    자바스크립트 코드는 타입을 지정하지 않기 때문에 매개변수로 전달하는 값이 숫자인지 문자열인지 검사하지 않습니다. 원래 의도라면 10 + 20 = 30이었겠지만 실제로는 두 개의 문자열 “10”과 “20”을 합친 "1020"가 결과로 반환됩니다.

    반면에 다음과 같이 타입스크립트는 매개변수의 타입을 지정합니다.

    1
    2
    3
    4
    5
    
    function add(a: number, b: number) {
      return a + b;
    }
    
    console.log(add(10, "20")); // Argument of type 'string' is not assignable to parameter of type 'number'.ts(2345)
    

    따라서 해당 타입스크립트 코드 컴파일 시점에 컴파일 에러가 발생하기 때문에 오류를 쉽게 잡아낼 수 있습니다.

  • 개발 도구(IDE나 컴파일러 등) 피드백

    타입스크립트 코드 작성 시에 IDE에서 함수 정의와 함수 매개변수 타입을 알고 있기 때문에 코드 자동 완성, 잘못된 변수/함수 사용에 대한 에러 알림과 같은 피드백을 받을 수 있습니다. 이를 통해 순수 자바스크립트를 사용하는 것에 비해 생산성이 크게 향상됩니다.

타입스크립트 개발 환경 구축 방법

터미널에서 다음 명령어를 입력하여 타입스크립트를 전역으로 설치합니다.

1
npm install -g typescript

이후 타입스크립트가 잘 설치되었는지 확인하기 위해 다음 명령어를 입력합니다.

1
tsc --version

pic1

타입스크립트를 실행할 폴더에서 tsconfig.json 파일을 생성합니다. npx tsc --init 명령어를 통해 tsconfig.json 파일을 초기화하여 생성할 수 있습니다.

pic2

tsconfig.json 파일이 생성되면 다음과 같은 타입스크립트 코드를 자바스크립트 코드로 컴파일할 수 있습니다.

1
2
3
4
5
6
7
// test.ts

function add(a: number, b: number) {
  return a + b;
}

console.log(add(10, 20));
1
tsc test.ts

pic3

위의 방식은 한 번에 하나의 타입스크립트 파일을 컴파일하는 방식입니다. 만약 특정 폴더 내의 모든 타입스크립트 파일들을 컴파일하고 싶다면 다음과 같이 tsconfig.json의 include 옵션에 폴더를 지정할 수 있습니다. 또한 outDir 옵션에 컴파일 결과 폴더를 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
{
  "compilerOptions": {
    // ...

    "outDir": "./dist"

    // ...
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

이후 다음 명령어를 입력하여 특정 폴더 내의 모든 타입스크립트 파일들을 컴파일할 수 있습니다.

1
tsc

pic4

위의 방식들은 모두 타입스크립트 파일을 자바스크립트 파일로 컴파일해 자바스크립트 파일을 실행하는 방식입니다. ts-node 패키지를 설치하면 컴파일 과정없이 바로 타입스크립트 코드를 실행할 수 있습니다. (참고로 ts-node도 내부적으로는 타입스크립트를 자바스크립트로 변환 후 실행합니다.)

1
npm install -g ts-node

이후 다음과 같이 타입스크립트 코드를 실행할 수 있습니다.

pic5

타입스크립트 문법

변수 선언

자바스크립트와 마찬가지로 var, let, const 3가지 키워드로 변수를 선언할 수 있습니다.

1
2
3
var a = 1;
let b = 2;
const c = 3;

타입 애너테이션

타입 애너테이션(Type Annotation)은 타입을 명시하는 방법을 의미합니다.

먼저 변수의 경우 다음과 같이 [변수명]: [타입] 형식으로 타입을 지정할 수 있습니다.

1
2
3
const a: number = 1;
const b: string = "2";
const c: boolean = true;

함수의 경우 매개변수는 [변수명]: [타입] 형식으로, 반환값은 매개변수 뒤에 타입을 명시하는 방식으로 지정합니다.

1
2
3
function add(a: number, b: number): number {
  return a + b;
}

객체의 경우 다음과 같이 타입을 지정할 수 있습니다.

1
2
3
4
5
const student: { name: string; age: number; height: number } = {
  name: "HyunJinNo",
  age: 27,
  height: 182.8
};

변수명 뒤에 ?를 붙여서 선택적 속성(Optional Property)으로 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const student: {
  name: string;
  age: number;
  height: number;
  gender?: string; // 선택적 속성
} = {
  name: "HyunJinNo";
  age: 27;
  height: 182.8;
};

function add(a: number, b: number, c?: number): number {
  if (c) {
    return a + b + c;
  }

  return a + b;
}

기본 타입

타입스크립트는 7가지 기본 자료형을 갖고 있습니다.

타입설명
nullnull은 의도적으로 값이 없음을 나타낼 때 사용합니다.
undefinedundefined는 변수에 값이 할당되지 않은 경우를 뜻합니다. 즉 의도하지 않은 값의 부재를 의미합니다.
boolean참(true)과 거짓(false) 두 가지 값을 가지고 있는 타입입니다.
string문자열 타입을 나타냅니다.
number숫자를 나타내는 타입입니다. 정수, 부동 소수점, 2진수, 8진수, 16진수도 숫자 타입에 해당합니다.
bigint매우 큰 숫자도 할당할 수 있는 타입입니다. 숫자 뒤에 n을 붙여서 123n으로 표시합니다.
symbol불변이면서 유니크한 값을 표현하는 자료형입니다.
1
2
3
4
5
6
7
const myNull: null = null;
const myUndefined: undefined = undefined;
const myBoolean: boolean = true;
const myString: string = "string";
const myNumber: number = 1;
const myBigint: bigint = 123n;
const mySymbol: symbol = Symbol("mySymbol");

배열과 튜플

배열과 튜플은 여러 개의 데이터를 저장한다면 점에서 비슷하지만, 배열은 크기가 고정이 아니며, 튜플은 크기가 고정된다는 점이 다릅니다.

배열은 각 원소의 타입을 정의하며, 다음 두 가지 방법으로 타입을 선언할 수 있습니다.

1
2
const arr1: number[] = [1, 2, 3];
const arr2: Array<number> = [1, 2, 3];

튜플은 배열과 달리 원소 개수만큼 타입을 정의해야 합니다. 튜플은 다음 방식으로 타입을 선언할 수 있습니다.

1
const tuple: [string, number] = ["string", 123];

any

any 타입은 unknown 타입을 제외하고 가장 상위에 있는 타입입니다. any는 타입을 모르거나 지정할 수 없을 때 유용할 수 있습니다. 보통 서드파티 라이브러리에서 코드에 타입이 없는 경우 any를 사용해 컴파일이 안되는 문제를 해결할 수 있습니다. any로 타입을 선언하는 경우 컴파일 시점이 아닌 런타임 시점에 에러가 날 가능성이 높으므로 any 타입 사용은 가능하면 지양하는 것이 좋습니다.

1
2
3
let anyValue = 10;
anyValue = "Hello, World!";
anyValue = true;

unknown

any와 같이 최상위 타입인 unknown 타입은 알 수 없는 타입을 의미합니다. any와 같이 unknown에는 어떤 타입의 값도 할당할 수 있지만, unknown 타입은 any 타입 외의 어떤 타입에도 할당할 수 없습니다. unknown 타입을 any 타입 외에 어떤 타입에 할당하려면 타입 단언이나 타입 가드가 필요하므로 any 타입보다 더 안전합니다.

1
2
3
4
5
6
let a: any = 123;
let b: unknown = 123;

let c: number = a;
let d: number = b as number;
let e: number = b; // Error: Type 'unknown' is not assignable to type 'number'.ts(2322)

void

void는 함수의 반환값에 지정하는 타입으로, 함수의 반환값이 없는 경우(실제로는 undefined를 반환)에 사용합니다.

1
2
3
function sayHello(): void {
  console.log("Hello, World!");
}

never

never는 함수의 반환값에 지정하는 타입으로, 의도적으로 값을 반환하지 않는 때 사용합니다. 예외를 발생시키는 함수이거나 무한 루프를 실행하는 함수가 이에 해당합니다.

1
2
3
function throwError(message: string): never {
  throw new Error(message);
}

Union

유니온(Union)은 2개 이상의 타입을 허용하는 것을 의미합니다. |(Vertical Bar) 기호를 사용하여 타입을 구분해 여러 타입을 정의합니다.

1
2
3
let anyValue: number | string | boolean = 10;
anyValue = "Hello, World!";
anyValue = true;

Intersection

&를 사용해 2개 이상의 타입을 조합하는 경우를 Intersection이라고 합니다. 여러 개의 타입 중 하나만 만족하면 되는 Union과 달리, Intersection은 조합한 모든 타입을 만족해야 합니다. Unionor에 해당한다면, Intersectionand에 해당합니다.

1
2
3
4
5
6
7
8
type Cup = { size: number };
type Brand = { brandName: string };
type BrandedCup = Cup & Brand;

let brandedCup: BrandedCup = {
  size: 100,
  brandName: "BrandName"
};

Intersection을 잘못 사용하면 다음과 같이 값을 할당할 수 없는 타입을 만드는 실수를 할 수 있습니다. Intersection을 사용할 때 주의해야 합니다.

1
2
type Impossible = number & string;
let impossible: Impossible = 10; // Error: Type '10' is not assignable to type 'never'.ts(2322)

타입 가드

Type Narrowing이란 타입의 범위를 좁히는 작업을 의미합니다. 타입의 범위를 좁히는 데 사용하는 검사 방법을 타입 가드(Type Guard)라 하며, 값 할당 또는 조건문으로 검사해 타입의 범위를 좁힙니다. 타입 가드를 선언하는 방법으로 typeof, instanceof, in 등이 있습니다.

typeof 연산자

typeof 연산자를 사용하면 원시 타입(string, number, boolean, symbol, bigint, function, object, undefined)를 구별할 수 있습니다. 단, typeof는 객체 타입(object, array, null)을 구별하기 어렵습니다.

1
2
3
4
5
6
7
function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length);
  } else {
    console.log(value.toFixed(2));
  }
}

instanceof 연산자

instanceof 연산자를 사용하면 객체가 특정 클래스의 인스턴스인지 확인할 수 있습니다. 단, instanceof는 클래스 기반 객체에서만 사용 가능하며, 인터페이스나 일반 객체에는 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat {
  meow() {
    console.log("meow");
  }
}

class Dog {
  bark() {
    console.log("bark");
  }
}

function makeSound(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow();
  } else {
    animal.bark();
  }
}

in 연산자

in 연산자를 사용하면 객체가 특정 속성을 가지고 있는지 확인할 수 있습니다. in 연산자는 인터페이스, 객체 리터럴, 클래스 등 여러 곳에서 활용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
type Person = { name: string; age: number };
type Robot = { model: string; battery: number };

function printInfo(entity: Person | Robot) {
  if ("name" in entity) {
    console.log(entity.name);
  } else {
    console.log(entity.model);
  }
}

is - 사용자 정의 타입 가드

is 키워드는 사용자 정의 타입 가드(User-Defined Type Guards)를 정의하는데 사용됩니다. 함수를 활용하여 타입 검사를 직접 수행하고 특정 타입임을 보장하는 방식으로, 더 복잡한 조건에서도 Type Narrowing을 적용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Person = { name: string };
type Robot = { model: string };
type Entity = Person | Robot;

function isPerson(entity: Entity): entity is Person {
  return (entity as Person).name !== undefined;
}

function PrintInfo(entity: Entity) {
  if (isPerson(entity)) {
    console.log(entity.name);
  } else {
    console.log(entity.model);
  }
}

const person: Person = { name: "HyunJinNo" };
PrintInfo(person); // "HyunJinNo"

Non-null assertion 연산자

!를 사용하는 Non-null assertion 연산자를 사용하여 피연산자의 값이 null이나 undefined가 아님을 단언할 수 있습니다.

1
2
3
4
5
6
7
function printLength1(str: string | undefined | null) {
  console.log(str.length); // Error: 'str' is possibly 'null' or 'undefined'.ts(18049)
}

function printLength2(str: string | undefined | null) {
  console.log(str!.length);
}

타입 단언

타입 단언(Type Assertion)은 컴파일러에게 특정 값의 타입을 알려주는 것을 말합니다. 타입을 강제 변환하는 것이 아니라, 컴파일러가 타입을 추론하는 방식에 개입하는 역할을 합니다.

타입 단언에는 두 가지 문법이 존재합니다.

<타입> 문법

<타입> 형식으로 타입 단언을 선언할 수 있습니다. 다만 해당 방식은 JSX 문법과 충돌할 수 있으며, 제네릭(Generic)과 헷갈릴 수 있으므로 사용이 지양됩니다.

1
2
let value: any = "Hello, World!";
let strLength: number = (<string>value).length;

as 문법

as 문법을 통해 타입 단언을 선언할 수 있습니다.

1
2
let value: any = "Hello, World!";
let strLength: number = (value as string).length;

타입 별칭

type 키워드를 사용하여 타입 별칭을 만들 수 있습니다.

1
2
3
4
type Length = number | string;

let len: Length = 10;
len = "10km";

리터럴 타입

리터럴 타입(Literal Type)이란 기본 타입의 값들을 조합해서 한정적인 값들만 나타내는 타입입니다. 다음과 같이 size10, 20, 30만 있다고 가정할 때 10, 20, 30만 허용하는 리터럴 타입을 정의할 수 있습니다.

1
2
3
4
5
6
type Size = 10 | 20 | 30;

let size1: Size = 10;
let size2: Size = 20;
let size3: Size = 30;
let size4: Size = 40; // Error: Type '40' is not assignable to type 'Size'.ts(2322)

인터페이스

인터페이스(Interface)타입 별칭보다 더 읽기 쉬운 오류 메세지, 더 빠른 컴파일러 성능, 클래스와 함께 사용할 수 있는 장점을 제공합니다.

typeinterface를 비교하면 다음과 같습니다.

typeinterface
기존 타입 또는 새 타입을 생성하는데 사용객체 타입의 구조를 정의하는데 사용
다른 타입 또는 인터페이스를 상속하거나 구현할 수 없음다른 인터페이스를 상속하거나 구현할 수 있음
리터럴 타입, Union 타입, Intersection 타입을 사용할 수 있음extends 키워드로 인터페이스 확장이 가능함
간단한 타입 별칭을 생성할 때 적합잘 정의된 구조의 객체 타입을 정의할 때 적합

typeinterface를 사용한 예시는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
type PersonType = { name: string; age: number };

interface Person {
  name: string;
  age: number;
}

let personType: PersonType = { name: "HyunJinNo", age: 27 };

let person: Person = { name: "HyunJinNo", age: 27 };

다음과 같이 extends 키워드를 사용하여 인터페이스 확장이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
  name: string;
  age: number;
}

interface Man extends Person {
  height: number;
}

const man: Man = {
  name: "HyunJinNo",
  age: 27,
  height: 182.8
};

클래스

class 키워드를 사용하여 클래스를 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
class Person {
  constructor(private firstName: string, private lastName: string) {}

  getName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const person = new Person("HyunJin", "No");
console.log(person.getName()); // "HyunJin No"

위의 코드에서 constructor(private firstName: string, private lastName: string) {}는 다음 코드와 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
  private firstName: string;
  private lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const person = new Person("HyunJin", "No");
console.log(person.getName()); // "HyunJin No"

implements

implements 키워드를 사용하여 인터페이스를 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IPerson {
  firstName: string;
  lastName: string;
  getName: () => string;
}

class Person implements IPerson {
  constructor(public firstName: string, public lastName: string) {}

  getName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const person = new Person("HyunJin", "No");
console.log(person.getName()); // "HyunJin No"

abstract

abstract 키워드를 사용하여 추상 클래스를 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface IPerson {
  firstName: string;
  lastName: string;
  getName: () => string;
}

abstract class Person implements IPerson {
  constructor(public firstName: string, public lastName: string) {}

  abstract getName(): string;
}

class Man extends Person {
  getName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const man = new Man("HyunJin", "No");
console.log(man.getName()); // "HyunJin No"

접근 제한자

타입스크립트에는 클래스 내에 변수와 메서드에서 사용할 수 있는 접근 제한자(Access Modifiers)들이 있습니다. 접근 제한자가 있는 변수와 메서드는 클래스 외부에서 해당 클래스의 구현을 공개할지 여부를 결정합니다. 접근 제한자는 타입스크립트의 기능으로, 자바스크립트로 컴파일된 후에는 제거됩니다.

타입스크립트에서 사용할 수 있는 접근 제한자의 종류는 다음과 같습니다.

접근 제한자설명
public모든 곳에서 접근 가능(기본값)
protected클래스 내부 또는 자식 클래스에서만 접근 가능
private클래스 내부에서만 접근 가능

readonly

readonly 키워드를 사용하면 처음 속성의 값이 결정되면 이후에는 변경할 수 없는 읽기 전용 속성을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
interface Person {
  readonly name: string;
  age: number;
}

let person: Person = { name: "HyunJinNo", age: 27 };

person.age = 28;
person.name = "HyunJin"; // Error: Cannot assign to 'name' because it is a read-only property.ts(2540)

유틸리티 타입

유틸리티 타입(Utility Types)은 기본 타입을 변환하거나 조작하는 데 사용할 수 있는 타입입니다. 유틸리티 타입을 사용하면 간결한 방식으로 타입을 정의하거나 이미 정의된 타입을 변환할 수 있습니다.

Partial

Partial을 사용하면 객체 내 모든 프로퍼티를 선택적 속성(Optional Property)으로 변경할 수 있습니다.

1
2
3
4
5
6
7
8
interface Point {
  x: number;
  y: number;
}

const point: Partial<Point> = { x: 1 };
console.log(point.x); // 1
console.log(point.y); // undefined

Required

Required를 사용하면 객체 내 모든 프로퍼티를 필수 속성으로 변경할 수 있습니다.

1
2
3
4
5
6
7
8
interface Point {
  x: number;
  y?: number;
}

const point: Required<Point> = { x: 1, y: 2 };
console.log(point.x); // 1
console.log(point.y); // 2

Record

Record를 사용하면 특정 key 타입과 value 타입을 갖는 객체 타입을 정의할 수 있습니다.

1
2
3
4
5
// Record<string, number> === { [key: string]: number }
const peopleAgeInfo: Record<string, number> = {
  Alice: 21,
  Bob: 22
};

Omit

Omit을 사용하면 한 객체 타입에서 특정 프로퍼티를 제외한 새로운 타입을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
interface Person {
  name: string;
  age: number;
  location: string;
  gender: string;
}

const person: Omit<Person, "location" | "gender"> = {
  name: "HyunJinNo",
  age: 27
};

Pick

Pick을 사용하면 한 객체 타입에서 특정 프로퍼티만 존재하는 객체 타입을 정의할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
interface Person {
  name: string;
  age: number;
  location: string;
  gender: string;
}

const person: Pick<Person, "name" | "age"> = {
  name: "HyunJinNo",
  age: 27
};

Exclude

Exclude를 사용하면 Union 타입에서 특정 타입을 제외한 새로운 Union 타입을 정의할 수 있습니다.

1
2
3
4
5
6
type Type1 = 10 | 20 | 30;
type Type2 = Exclude<Type1, 20>; // type Type2 = 10 | 30;

type Type3 = string | number | boolean;
let value: Exclude<Type3, number> = "string";
value = true;

ReturnType

ReturnType을 사용하면 특정 함수의 반환 타입으로 구성된 타입을 정의할 수 있습니다.

1
2
3
4
5
6
7
const myFunc = () => {
  return "a";
};

type MyType = ReturnType<typeof myFunc>;
const value1: MyType = "a";
const value2: ReturnType<typeof myFunc> = "b";

Parameters

Parameters를 사용하면 함수의 매개변수 타입들의 튜플 타입을 정의할 수 있습니다.

1
2
3
4
5
type MyType1 = (s: string, flag: boolean) => void;
type MyType2 = (num: number) => number;

type MyType3 = Parameters<MyType1>;
const value: MyType3 = ["string", true];

Readonly

Readonly를 사용하면 객체 내 모든 프로퍼티를 읽기 전용(readonly)으로 변경할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
interface Person {
  name: string;
  age: number;
}

const person: Readonly<Person> = {
  name: "HyunJinNo",
  age: 27
};

person.name = "HyunJin"; // Error: Cannot assign to 'name' because it is a read-only property.ts(2540)

참고 자료

This post is licensed under CC BY 4.0 by the author.