타입스크립트 CheatSheet

yeim·2023년 8월 6일
139
post-thumbnail

이 글은 JavaScript의 기본적인 문법들을 알고 어느정도 활용할 수 있다는 가정하에 작성되어 있습니다. 그렇기 때문에 JavaScript가 낯선 분들에게는 다소 어려운 글이 될 수도 있습니다.

타입스크립트를 처음 접하시는 분들은 'TypeScript 개요', 'TypeScript를 사용하는 이유'를 읽고 'TypeScript 일단 시작해보기'까지 해보신 이후에 작은 프로젝트를 시작해보며 'Usage'에 나온 사용법들을 CheatSheet처럼 참고하고, 하나씩 적용해 보는 것을 추천드립니다. (한 번에 다 읽기에는 양이 너무 많고, 비효율적입니다ㅠ)

타입스크립트 프로젝트 경험이 어느정도 있으신 분들에게는 다소 부끄러운 글이 될 수 있겠지만 가벼운 마음으로 전체적인 흐름을 훑어볼 수 있는 글이 되었으면 좋겠습니다. (잘못되거나 추가되면 좋을 것 같은 부분들은 댓글 남겨 주시면 감사하겠습니다🙇🏻)


TypeScript란?


타입스크립트(TypeScript)는 Microsoft에서 개발한 오픈 소스 프로그래밍 언어입니다. 타입스크립트는 자바스크립트(JavaScript)의 상위 집합(super set)으로서, 자바스크립트의 모든 기능을 포함하면서 추가적인 정적 타입 기능을 제공합니다.

정적 타입과 동적 타입

정적 타입은 변수의 타입을 컴파일 시점에 결정하는 방식을 의미하고, 동적 타입은 변수의 타입을 런타임 시점에 결정하는 방식을 의미합니다. JavaScript는 동적 타입 언어이며, TypeScript는 정적 타입 언어입니다.

// TypeScript (정적 타입)
function addNumbers(a: number, b: number): number {
  return a + b;
}

// JavaScript (동적 타입)
function addNumbers(a, b) {
  return a + b;
}
const result = addNumbers("안녕", "안녕");

위의 예시에서 보면 JavaScript는 동적 타입이기 때문에 런타임 시점에 addNumbers함수가 호출될 때 매개변수의 타입을 추론해 변수의 타입을 문자열로 결정합니다. 반면, TypeScript는 정적 타입이기 때문에 런타임 이전의 컴파일 시점에서 이미 매개변수의 타입은 number 입니다.

타입스크립트 컴파일러(tsc)

타입스크립트는 자바스크립트의 상위 집합이기 때문에, 타입스크립트 코드를 실행하려면 먼저 TypeScript코드를 JavaScript코드로 변환해야 합니다. 이 변환은 타입스크립트 컴파일러(tsc)를 통해 이루어 지며, 주요 기능은 아래와 같습니다.

  1. 타입 체크 (Type Checking): tsc는 TypeScript 코드에서 타입 에러를 체크하여 런타임 이전에 발견하고 오류를 보고합니다. 이를 통해 개발자가 타입 에러를 미리 확인하고 수정할 수 있도록 도와줍니다.
  2. ES 버전 변환 (ES Version Transpilation): tsc는 TypeScript 코드를 지정된 ECMAScript(ES) 버전으로 변환합니다. 최신 ES 버전에서 구형 ES 버전으로 변환하여 특정 환경에서도 실행할 수 있도록 지원합니다.
  3. 모듈 지원 (Module Support): tsc는 모듈 시스템을 지원하며, 다양한 모듈 형식으로 컴파일할 수 있습니다. CommonJS, AMD, ES Modules 등을 지원합니다.
  4. 소스 맵 생성 (Source Map Generation): tsc는 컴파일된 JavaScript 코드와 원본 TypeScript 코드 사이의 매핑 정보를 포함한 소스 맵(Source Map)을 생성합니다. 이를 통해 디버깅을 용이하게 합니다.
  5. 커스텀 설정 (Custom Configuration): tsc는 tsconfig.json 파일을 통해 컴파일 옵션을 지정할 수 있습니다. 이를 사용하여 프로젝트별로 컴파일 옵션을 관리할 수 있습니다.

TypeScript를 사용하는 이유


저는 코딩테스트에서 JavaScript를 많이 사용했었는데, 그 이유는 Type을 작성하지 않기 때문에 정해진 시간동안 빠른 속도로 코드를 작성해 나갈 수 있었기 때문입니다. (같은 이유에서 파이썬을 사용하시는 분들도 많이 있었습니다.) 하지만 프로젝트가 커지고, 기간이 길어지면 타입이 없다는게 커다란 단점으로 다가와서 TypeScript를 적용해보게 됐습니다.

이처럼 이 글을 읽으시는 분들도 TypeScript를 사용하려는 각자만의 이유를 가지고 계실 겁니다. 많은 사람들이 말하는 타입스크립트를 사용하는 주요 이유를 예제와 함께 적어보았습니다.

타입 안정성(Type Safety)

타입스크립트는 정적 타입을 지원하므로 변수의 타입을 명시하거나 추론하여 코드의 타입 안정성을 제공합니다. 이로 인해 개발자는 컴파일 단계에서 타입 관련 오류를 발견할 수 있습니다.

// JavaScript
function add(a, b) {
  return a + b;
}

console.log(add(5, "10")); // 결과: "510"

위의 자바스크립트 코드에서는 add 함수가 첫 번째 인자 a와 두 번째 인자 b를 단순히 더하도록 작성되어 있습니다. 하지만 두 번째 인자 b에 문자열 "10"이 전달되었습니다. 자바스크립트는 동적 타입 언어이므로 "5"와 "10"을 단순히 문자열로 이어붙여서 "510"을 반환합니다. 이는 의도하지 않은 결과이며, 숫자 덧셈으로 기대되는 결과와 다릅니다.

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

console.log(add(5, "10"));
// 에러: Argument of type '"10"' is not assignable to parameter of type 'number'.

위의 타입스크립트 코드에서는 add 함수의 매개변수 a와 b에 각각 number 타입을 명시했습니다. 두 번째 인자로 문자열 "10"을 전달하면 타입스크립트 컴파일러에서 타입 오류를 발견합니다.

코드 가이드와 자동 완성

타입스크립트는 코드 작성 시 타입 정보를 활용하여 개발 도구에서 더 나은 코드 가이드와 자동 완성 기능을 제공합니다. 이는 개발자가 더 빠르고 정확하게 코드를 작성할 수 있도록 도와줍니다.

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

const result = add(10, 20):

result. // 여기서 age를 작성하려고 할 때, 자동으로 Number타입에서 사용할 수 있는 메소드와 필드들을 추천해줍니다.

JavaScript로 작성하는 코드들도 일부 코드 가이드를 제공해주지만, 런타임에 타입을 추론을 해야하는 방식이기 때문에, 함수의 반환값이나 매개변수에 대해 타입을 제공받기는 어렵습니다. 반면 TypeScript는 컴파일러에게 사용하는 타입을 명시적으로 지정해주기 때문에, 개발자가 더 많은 코드 가이드와 자동 완성을 제공받을 수 있습니다.

리팩토링과 유지보수성

타입스크립트는 Enum, 인터페이스 등의 개념을 사용할 수 있습니다. 이는 대규모 프로젝트에서 코드의 유지보수성을 향상시키고 리팩토링을 더 안전하고 쉽게 할 수 있도록 도와줍니다.

// JavaScript
const user = {
  name: "John Park",
  age: 30,
};

user.id = "John"; //id 프로퍼티가 추가됨

위의 JavaScript 코드에서 user 객체를 주고 받다보면, 특정 프로퍼티 값을 변경하거나 참조해야 하는 상황이 올 수 있습니다. 프로젝트의 크기가 커져 해당 객체에 프로퍼티의 값을 잘못 참조해 추가하는 경우가 올 수 있습니다.

// TypeScript
interface User {
  name: string;
  age: number;
}

const user: User = {
  name: "John Park",
  age: 30,
};

user.id = "John";
//'User' 형식에 'id' 속성이 없습니다.ts(2339)

User라는 인터페이스를 정의하여 사용자 정보의 타입을 명시하였습니다. 이를 통해 사용자 정보 객체가 어떤 속성을 가져야 하는지를 명확하게 정의하고 코드의 가독성을 향상시킵니다. user 변수에 타입 정보를 명시하여 초기화합니다. 이를 통해 user 변수가 User 인터페이스에 맞는 객체임을 컴파일러가 확인하고 올바르지 않은 속성이 추가되는 것을 방지합니다.


TypeScript 일단 시작해보기


'TypeScript 개요' 에서 말씀드린 tsc를 통해서 타입스크립트를 시작해보겠습니다. 아래의 단계들을 통해 타입스크립트 환경을 구성하고 기본적인 코드를 작성해볼 수 있습니다.

1. Node.js 및 npm 설치

타입스크립트를 사용하기 위해서는 먼저 Node.js와 npm(Node Package Manager)를 설치해야 합니다. Node.js는 타입스크립트 코드를 실행하는 런타임 환경을 제공합니다. npm은 Node.js 패키지를 관리하는 패키지 매니저로, 타입스크립트를 포함한 라이브러리를 설치하고 관리하는데 사용됩니다.

2. 타입스크립트 설치 및 프로젝트 초기화

$ npm install -g typescript

npm을 사용하여 타입스크립트를 글로벌로 설치합니다.

그리고, 아래의 명령들을 통해 타입스크립트 프로젝트를 초기화합니다.

$ mkdir my-ts-project
$ cd my-ts-project
$ tsc --init

타입스크립트 프로젝트를 시작하기 위해 프로젝트 폴더를 만들고, tsconfig.json 파일을 생성하여 초기화합니다.

3. Typescript 코드 작성

// app.ts
function sayHello(name: string) {
  console.log(`Hello, ${name}!`);
}
sayHello('TypeScript'); // 출력: "Hello, TypeScript!"

타입스크립트 코드를 작성하고 컴파일하여 JavaScript 코드를 생성합니다. TypeScript 코드는 .ts 확장자로 저장하고, 컴파일하면 .js 파일이 생성됩니다.

4. 타입스크립트 코드 컴파일, 실행

$ tsc app.ts

타입스크립트 코드를 컴파일하여 JavaScript 코드로 변환합니다.

컴파일된 JavaScript 코드를 실행하거나, 웹 브라우저에서 스크립트로 사용하거나, Node.js에서 실행하여 결과를 확인합니다.

$ node app.js #"Hello, TypeScript!" 출력

타입스크립트 프로젝트를 초기화하고 기본적인 코드를 작성하여 실행해보았습니다. 위의 typescript를 global로 설치하지 않고도 현재 프로젝트에만 install해서 사용하는 방법도 있으며, ts-node를 통해 컴파일과 실행을 동시에 하는 방법도 있습니다. (ts-node는 주로 개발 환경에서 사용되며, 프로덕션 환경에서는 일반적으로 tsc로 빌드한 JavaScript파일을 실행하는 것이 권장됩니다.)


Usage - 기본문법


이제 TypeScript를 사용하는데 알아야 할 핵심 문법들을 예제와 함께 알아보겠습니다. 여기서부터는 직접 타이핑 해보시는 것을 적극 추천드립니다:>

TS의 타입

TypeScript는 JavaScript의 상위집합(superset)이기 때문에 JavaScript에서 제공되는 기본 자료형을 모두 포함하고, 프로그래밍을 도울 몇 가지 타입이 더 제공됩니다. 우선 JavaScript의 기본 자료형을 간단히 알아본 후 TypeScript에서 추가된 자료형을 확인해 보겠습니다.

JS의 기본 7가지 타입: JavaScript는 기본적으로 아래와 같은 7가지 기본 자료형을 지원합니다.

let age: number = 30;
let name: string = "Alice";
let isStudent: boolean = true;
let data: null = null;
let value: undefined = undefined;
let user: object = { name: "Bob", age: 25 };
let key: symbol = Symbol("key");
  • number: 숫자를 나타내는 타입입니다.

  • string: 문자열을 나타내는 타입입니다.

  • boolean: true 또는 false 값을 나타내는 타입입니다.

  • null: 값이 없음을 나타내는 타입입니다.

  • undefined: 초기화되지 않은 값 또는 없는 값을 나타내는 타입입니다.

  • object: 객체를 나타내는 타입으로, 배열, 함수 등도 포함됩니다.

  • symbol: 고유하고 변경 불가능한 값으로 사용되는 타입입니다.

Tuple (튜플): TypeScript에서 지원하는 고정된 개수의 요소로 이루어진 배열 타입으로, 각 요소는 서로 다른 타입일 수 있습니다.

let tuple: [string, number] = ["Alice", 30]

Enum (열거형): 숫자 또는 문자열의 상수 값을 이름으로 정의할 수 있도록 도와줍니다. (Enum 관련해서는 잠시 후에 더 자세히 알아보겠습니다.)

enum Color {
  Red,
  Green,
  Blue,
}

let color: Color = Color.Green;

Any: 모든 타입을 허용하는 동적 타입으로, 타입 검사를 우회하는 용도로 사용될 수 있습니다.

let dynamicValue: any = "Hello";
dynamicValue = 42;

Unknown: any와 유사하지만, 타입 검사를 강제하는 타입입니다. any보다 더 안전한 타입이며, 사용하기 전에 타입 검사를 거쳐야 합니다.

let unknownValue: unknown = "Hello";
// unknownValue.length; // 오류: unknown 타입은 프로퍼티에 접근할 수 없습니다.

Void: 함수가 반환하지 않는 경우에 사용되는 타입으로, 반환 값이 없음을 명시합니다.

function logMessage(): void {
  console.log("Hello!");
}

Never: 결코 발생하지 않는 값의 타입으로, 항상 예외 또는 무한 루프와 같은 비정상적인 동작을 의미합니다.

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

Literal Types: 문자열 또는 숫자 값 자체를 타입으로 지정할 수 있습니다.

let gender: "male" | "female" = "male";
let grade: 1 | 2 | 3 | 4 = 3;

타입 시스템

타입 시스템에는 두 가지가 있습니다.

  1. 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
  2. 컴파일러가 자동으로 타입을 추론하는 시스템

타입 스크립트의 타입 시스템은 두 가지가 다 사용 가능한데, 기본적으로 타입을 명시적으로 지정할 수 있고, 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론합니다. 타입스크립트는 위의 두 시스템을 어떻게 지원하는지 코드와 함께 살펴봅시다.

타입 주석(type annotation): 타입 주석은 변수나 함수의 매개변수 뒤에 콜론(:)을 사용하여 타입을 지정하는 것을 말합니다.

let age: number;
function greet(name: string): string {
  return `Hello, ${name}!`;
}

타입 추론(type interface): 타입 추론은 TypeScript가 변수나 함수의 타입을 자동으로 유추하는 기능입니다.

let age = 30; // 타입 추론에 의해 number 타입으로 추론됩니다.
let name = "Alice"; // 타입 추론에 의해 string 타입으로 추론됩니다.

function greet(name: string) {
  return `Hello, ${name}!`; // 반환 값의 타입으로 string 타입으로 추론됩니다.
}

타입 주석을 사용하면 변수나 함수에 명시적으로 타입을 지정하여 가독성을 높일 수 있고, 타입 추론을 활용하여 코드를 더 간결하고 유연하게 작성할 수 있습니다.

함수

다음으로는 JavaScript 함수관련 문법들이 TypeScript를 만났을때 어떻게 활용될 수 있는지 예제들로 살펴보겠습니다. 각 개념들은 JavaScript개념들과 일치하기 때문에 관련해서 자세한 내용은 많이 생략되었습니다.

함수 선언문과 함수 표현식: 함수 선언문function 키워드를 사용하여 함수를 선언하는 방식입니다. 함수 선언문은 호이스팅에 영향을 받으므로 함수 정의가 코드 어디에 있더라도 호출할 수 있습니다. 함수 표현식은 변수에 함수를 할당하는 방식입니다. 함수 표현식은 호이스팅에 영향을 받지 않으므로 함수 정의가 선언되기 전에 호출하면 에러가 발생합니다.

//함수 선언문
function add(a: number, b: number): number {
  return a + b;
}

//함수 표현식
const add = function(a: number, b: number): number {
  return a + b;
};

매개변수와 반환 타입 지정: 함수에서 매개변수와 반환 타입을 지정하는 방법은 다음과 같습니다.

function greet(name: string): string {
  return `Hello, ${name}!`;
}

위의 예제에서 name은 매개변수로 타입이 string으로 지정되었습니다. 함수가 string을 반환하도록 지정하기 위해 : string을 사용하였습니다.

선택적 매개변수와 기본 매개변수 선택적 매개변수는 매개변수 이름 뒤에 ?를 붙여서 정의합니다. 이렇게 정의된 매개변수는 호출 시 값을 전달해도 되고 전달하지 않아도 됩니다. 기본 매개변수는 매개변수에 기본값을 설정하는 것으로, 매개변수에 값이 전달되지 않으면 기본값이 사용됩니다.

// name 선택적 매개변수
function greet(name?: string): string {
  if (name) {
    return `Hello, ${name}!`;
  } else {
    return `Hello, Guest!`;
  }
}

// name 기본 매개변수
function greet(name: string = "Guest"): string {
  return `Hello, ${name}!`;
}

Rest 파라미터: Rest 파라미터는 함수의 매개변수로서 나머지 인수들을 배열로 받을 수 있게 해줍니다. Rest 파라미터는 함수 정의에서 마지막 매개변수로만 사용될 수 있습니다.

function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

화살표 함수: 화살표 함수는 간결한 함수 표현식으로, => 기호를 사용하여 함수를 정의합니다. 화살표 함수는 항상 익명 함수로 사용되며, this를 어휘적으로(lexical) 바인딩하여 function 키워드 함수와 달리 자신만의 this를 가지지 않습니다.

const add = (a: number, b: number): number => a + b;

배열과 튜플

배열 타입 (Array Type): 배열 타입은 동일한 타입의 값들을 순차적으로 저장하는 자료 구조입니다. TypeScript에서 배열 타입을 정의하는 방법은 아래와 같습니다.

let numbers: number[] = [1, 2, 3, 4, 5];
let fruits: string[] = ["apple", "banana", "orange"];

튜플 타입 (Tuple Type): 튜플 타입은 고정된 요소 수와 각 요소의 타입을 미리 지정하는 배열의 한 유형입니다. 튜플은 배열과 비슷하지만, 튜플은 요소마다 서로 다른 타입을 가질 수 있으며, 요소의 수가 고정됩니다. 튜플 타입을 정의할 때는 [타입1, 타입2, ...] 형태로 사용합니다.

function getNameAndAge(): [string, number] {
  return ["Bob", 25];
}

const [name, age] = getNameAndAge();
console.log(name); // Output: Bob
console.log(age); // Output: 25

흔히 튜플은 위와 같이 함수에서 여러 개의 값을 반환할 때 사용되는데, 이때 함수의 반환 타입으로 튜플을 사용하면 각각의 값을 구분해서 사용할 수 있습니다.


튜플은 요소의 수와 타입을 미리 정의하기 때문에 정해진 구조의 데이터를 다룰 때 유용합니다. 하지만 튜플의 사용은 배열과 달리 유연성이 떨어질 수 있으므로, 상황에 맞게 적절히 선택하여 사용하는 것이 중요합니다.

Enum

Enum(열거형)은 TypeScript에서 특정 값들의 집합을 나타내는 자료형입니다. 숫자 기반 Enum과 문자 기반 Enum 두 가지 형태로 정의할 수 있습니다.

숫자 기반 Enum (Numeric Enums): 숫자 기반 Enum은 각 멤버에 숫자 값을 직접 할당하는 방식으로 정의됩니다. 이러한 Enum은 처음 멤버에 값을 할당하지 않으면 자동으로 0부터 시작하여 1씩 증가하는 값을 갖게 됩니다. 숫자 기반 Enum은 반드시 숫자 값으로 초기화해야 합니다.

enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right,  // 3
}

let move: Direction = Direction.Left;
console.log(move); // Output: 2

위의 예제에서 Direction Enum은 숫자 기반으로 정의되었습니다. 따라서 Up, Down, Left, Right는 각각 0, 1, 2, 3의 값을 가집니다. move 변수에 Direction.Left를 할당하면 해당 Enum 멤버의 값인 2가 출력됩니다.

문자 기반 Enum (String Enums): 문자 기반 Enum은 각 멤버에 문자열 값을 직접 할당하는 방식으로 정의됩니다. 이러한 Enum은 숫자 대신 문자열 값을 가집니다.

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

let move: Direction = Direction.Left;
console.log(move); // Output: LEFT

위의 예제에서 Direction Enum은 문자 기반으로 정의되었습니다. Up, Down, Left, Right 각각의 멤버는 "UP", "DOWN", "LEFT", "RIGHT"의 문자열 값을 가집니다. move 변수에 Direction.Left를 할당하면 해당 Enum 멤버의 값인 "LEFT"가 출력됩니다.

문자 기반 Enum은 숫자 대신 의미있는 문자열 값을 가지므로 가독성을 높이는데 유용합니다. 하지만 숫자 기반 Enum보다는 번거롭고 복잡한 멤버 정의가 필요하며, 문자열 값의 중복에 대한 주의가 필요합니다.


TypeScript에서 Enum은 기본적으로 숫자 기반으로 동작하며, 숫자나 문자열로 직접 값을 할당할 수 있습니다. 단, 다른 타입으로 할당할 수는 없습니다. 예를 들어, 다음과 같이 부울(Boolean) 값을 할당하는 것은 허용되지 않습니다.

enum Direction {
  Up = true, // 에러: 숫자나 문자열이 아닌 타입 할당 불가능
}

인터페이스와 타입

인터페이스(Interface): 인터페이스는 TypeScript에서 객체의 구조(shape)를 정의하는 방법입니다. 객체의 프로퍼티와 메서드의 타입을 지정하여 객체가 일정한 규칙에 따르도록 강제합니다. 인터페이스는 클래스나 다른 객체 리터럴 등에서 구현(implement)될 수 있습니다.

// 인터페이스 정의
interface Person {
  name: string;
  age: number;
  greet(): void;
}

// 객체가 인터페이스를 구현해야 합니다.
const person: Person = {
  name: "Alice",
  age: 30,
  greet() {
    console.log(`Hello, I'm ${this.name}.`);
  },
};

person.greet(); // Output: Hello, I'm Alice.

타입(Type): 타입은 객체 뿐만 아니라 모든 유형의 타입을 지정하는 데 사용됩니다. 인터페이스와 마찬가지로 객체의 구조를 정의할 수 있지만, 타입은 객체 외의 타입도 정의할 수 있습니다. 타입 별칭(type alias)을 사용하여 재사용 가능한 타입을 만들 수 있습니다.

// type alias을 이용한 정의
type Person = {
  name: string;
  age: number;
  greet(): void;
};

// 객체가 type alias를 구현해야 합니다.
const person: Person = {
  name: "Alice",
  age: 30,
  greet() {
    console.log(`Hello, I'm ${this.name}.`);
  },
};

person.greet(); // Output: Hello, I'm Alice.

인터섹션(Intersection): 인터섹션 타입은 & 기호를 사용하여 두 개 이상의 타입을 결합하는 방법입니다. 인터섹션 타입을 사용하면 여러 타입의 모든 특성이 포함된 단일 타입을 만들 수 있습니다.

type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: number;
  department: string;
};

type EmployeePerson = Person & Employee;

const employeePerson: EmployeePerson = {
  name: "Alice",
  age: 30,
  employeeId: 123,
  department: "HR",
};

유니온(Union): 유니온 타입은 | 기호를 사용하여 두 개 이상의 타입 중 하나를 선택적으로 지정하는 방법입니다. 유니온 타입을 사용하면 변수나 함수의 매개변수 등에 여러 타입의 값을 동시에 허용할 수 있습니다.

type Status = "active" | "inactive" | "pending";

function setStatus(status: Status) {
  console.log(`Status: ${status}`);
}

setStatus("active"); // Output: Status: active
setStatus("pending"); // Output: Status: pending
setStatus("invalid"); // Error: Argument of type 'string' is not assignable to parameter of type 'Status'.

interface와 type alias는 많은 부분에서 유사하며, 대부분의 경우 두 가지를 바꿔서 사용해도 동일한 결과를 얻을 수 있습니다. 차이점으로는 type alias는 모든 타입을 선언할 때 사용될 수 있고, interface는 객체에 대한 타입을 선언할 때에만 사용될 수 있다는 점입니다. 즉, 둘 다 객체에 대한 타입을 선언하는 데 사용될 수 있는데, 확장 측면에서 사용 용도가 달라지니 이를 고려해서 사용해야 합니다.

(일반적으로 확장이 불가능한 타입을 선언할때에는 type alias를 사용하고, 확장이 가능한 타입을 선언하면 선언 병합이 가능한 interface를 사용하는 것이 관례적입니다)

클래스

클래스 선언과 생성자 (Class Declaration and Constructor): JavaScript에서와 동일하게 TypeScript에서도 class키워드를 통해 클래스를 선언하고, 생성자(constructor)를 통해 인스턴스 생성하고 초기화합니다.

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);

person1.greet(); // Output: Hello, my name is Alice and I'm 30 years old.
person2.greet(); // Output: Hello, my name is Bob and I'm 25 years old.

상속 (Inheritance): 클래스는 다른 클래스를 상속할 수 있습니다. 상속을 통해 기존 클래스의 특성을 재사용하고 확장할 수 있습니다. 이 부분도 Javascript와 동일하게 extends 키워드를 사용하여 상속할 수 있습니다.

class Employee extends Person {
  employeeId: number;

  constructor(name: string, age: number, employeeId: number) {
    super(name, age); // 부모 클래스(Person)의 생성자 호출
    this.employeeId = employeeId;
  }

  introduce() {
    console.log(`Hello, I'm ${this.name}. My employee ID is ${this.employeeId}.`);
  }
}

const employee = new Employee("Carol", 28, 12345);
employee.greet(); // Output: Hello, my name is Carol and I'm 28 years old.
employee.introduce(); // Output: Hello, I'm Carol. My employee ID is 12345.

접근 제어자 (Access Modifiers): TypeScript는 클래스 멤버(프로퍼티, 메서드)의 접근 범위를 지정할 수 있습니다. (JavaScript도 #기호를 통해 private지정을 할 수 있습니다) TypeScript에서는 세 가지 접근 제어자를 제공합니다: public, private, protected.

class Person {
  public name: string;     // public 프로퍼티
  private age: number;     // private 프로퍼티
  protected email: string; // protected 프로퍼티

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }

  public introduce(): void {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }

  protected getAge(): number {
    return this.age;
  }
}

class Employee extends Person {
  private employeeId: number;

  constructor(name: string, age: number, email: string, employeeId: number) {
    super(name, age, email);
    this.employeeId = employeeId;
  }

  public showEmployeeInfo(): void {
    console.log(`Employee ID: ${this.employeeId}`);
    console.log(`Email: ${this.email}`);
    // console.log(`Age: ${this.age}`);  // 에러 발생 (private 프로퍼티는 자식 클래스에서 접근 불가능)
    console.log(`Age (from parent): ${this.getAge()}`); // 정상 실행 (protected 메서드는 자식 클래스에서 접근 가능)
  }
}

const person = new Person("Alice", 30, "alice@example.com");
person.introduce(); // Output: Hello, my name is Alice and I'm 30 years old.
// console.log(person.age); // 에러 발생 (private 프로퍼티는 클래스 외부에서 접근 불가능)
// console.log(person.email); // 에러 발생 (protected 프로퍼티는 클래스 외부에서 접근 불가능)

const employee = new Employee("Bob", 25, "bob@example.com", 12345);
employee.showEmployeeInfo();
// Output:
// Employee ID: 12345
// Email: bob@example.com
// Age (from parent): 25
// console.log(employee.age); // 에러 발생 (private 프로퍼티는 클래스 외부에서 접근 불가능)
// console.log(employee.email); // 에러 발생 (protected 프로퍼티는 클래스 외부에서 접근 불가능)

위의 예제에서 Person 클래스는 name, age, email 세 개의 프로퍼티를 가지며, 이들은 각각 public, private, protected로 선언되어 있습니다. Employee 클래스는 Person 클래스를 상속받으며, showEmployeeInfo 메서드에서 employeeId를 출력하고 getAge() 메서드를 호출하여 부모 클래스인 Personage 프로퍼티에 접근하는 예제입니다.

  • public: 기본 접근 제어자로, 클래스 외부에서 접근이 가능합니다.
  • private: 클래스 내부에서만 접근이 가능하며, 클래스 외부에서는 접근할 수 없습니다.
  • protected: 클래스 내부와 자식 클래스에서 접근이 가능하며, 클래스 외부에서는 접근할 수 없습니다.

readonly: 속성이나 인덱서(배열이나 객체 등)를 읽기 전용으로 선언하는 데 사용됩니다. 이는 해당 속성이나 인덱서가 선언된 이후에는 값을 수정할 수 없음을 의미합니다. 이로써 불변성을 유지하거나 의도치 않은 값의 변경을 방지할 수 있습니다.

class Person {
    readonly name: string;
    constructor(name: string) {
        this.name = name;
    }
}
let john = new Person("John");
john.name = "John"; // error! name is readonly.

위처럼 readonly를 사용하면 constructor() 함수에 초기 값 설정 로직을 넣어줘야 하므로 아래와 같이 인자에 readonly 키워드를 추가해서 코드를 줄일 수 있습니다.

class Person {
  readonly name: string;
  constructor(readonly name: string) { }
}

Usage - 심화문법


제네릭

제네릭(Generics)은 TypeScript에서 함수, 클래스, 인터페이스를 사용할 때 타입을 매개변수화하여 재사용성을 높이는 기능입니다. 제네릭을 사용하면 동일한 로직을 다양한 타입에 대해 재사용할 수 있으며, 타입 안정성을 유지할 수 있습니다.

제네릭 함수 (Generic Functions): 제네릭 함수는 함수를 정의할 때 매개변수의 타입을 일반화하여 여러 타입에서 사용할 수 있도록 합니다. 제네릭 함수를 정의할 때는 <T>와 같이 <> 사이에 타입 매개변수를 선언합니다.

function identity<T>(arg: T): T {
  return arg;
}

let result1 = identity<number>(10); // T가 number로 유추됨
let result2 = identity<string>("hello"); // T가 string으로 유추됨

console.log(result1); // Output: 10
console.log(result2); // Output: hello

제네릭 함수 identity는 하나의 매개변수 arg를 받고, 이를 그대로 반환하는 함수입니다. identity 함수를 호출할 때 <number>와 같이 타입 인수를 명시적으로 지정해주거나, TypeScript가 매개변수 타입을 유추하게 할 수도 있습니다.

제네릭 클래스 (Generic Classes): 제네릭 클래스는 클래스를 정의할 때 클래스 내에서 사용되는 타입을 일반화하여 재사용성을 높이는 기능입니다. 제네릭 클래스를 정의할 때도 <T>와 같이 타입 매개변수를 선언합니다.

class Box<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

const numberBox = new Box<number>(10);
const stringBox = new Box<string>("hello");

console.log(numberBox.getValue()); // Output: 10
console.log(stringBox.getValue()); // Output: hello

위의 예제에서 Box 클래스는 하나의 제네릭 타입 매개변수 T를 가지고 있습니다. value 프로퍼티와 getValue 메서드는 이 제네릭 타입 T를 사용하여 선언되었습니다. Box 클래스를 사용할 때 <number>와 같이 타입 인수를 지정해주면 해당 타입에 맞는 Box 인스턴스가 생성됩니다.

제네릭 인터페이스 (Generic Interfaces): 제네릭 인터페이스는 인터페이스를 정의할 때 인터페이스 내에서 사용되는 타입을 일반화하여 재사용성을 높이는 기능입니다. 제네릭 인터페이스를 정의할 때도 <T>와 같이 타입 매개변수를 선언합니다.

interface Pair<T, U> {
  first: T;
  second: U;
}

const pair1: Pair<number, string> = { first: 10, second: "hello" };
const pair2: Pair<string, boolean> = { first: "world", second: true };

console.log(pair1); // Output: { first: 10, second: "hello" }
console.log(pair2); // Output: { first: "world", second: true }

위의 예제에서 Pair 인터페이스는 두 개의 제네릭 타입 매개변수 TU를 가지고 있습니다. Pair 인터페이스를 사용할 때 <number, string>과 같이 타입 인수를 지정해주면 해당 타입에 맞는 Pair 객체를 생성할 수 있습니다.

타입 가드와 타입 추론

타입 가드(Type Guards)와 타입 추론(Type Inference)은 TypeScript에서 타입을 더 정확하게 추론하고 검사하는데 사용되는 기능입니다. 각각의 기능에 대해서 자세히 알아보겠습니다.

typeof 타입 가드 (typeof Type Guard): typeof 타입 가드는 JavaScript의 typeof 연산자를 사용하여 변수의 타입을 검사하는 방법입니다. typeof를 사용하면 변수의 타입을 확인하고 해당 타입에 따라 코드 블록을 실행하거나 다른 작업을 수행할 수 있습니다.

function printType(x: number | string): void {
  if (typeof x === "number") {
    console.log("x is a number.");
  } else if (typeof x === "string") {
    console.log("x is a string.");
  } else {
    console.log("x is of unknown type.");
  }
}

printType(10); // Output: x is a number.
printType("hello"); // Output: x is a string.
printType(true); // Output: x is of unknown type.

위의 예제에서 printType 함수는 number 또는 string 타입의 매개변수 x를 받습니다. typeof x === "number"와 같이 typeof를 사용하여 변수 x의 타입을 검사하고, 해당하는 타입에 따라 다른 작업을 수행합니다.

instanceof 타입 가드 (instanceof Type Guard): instanceof 타입 가드는 JavaScript의 instanceof 연산자를 사용하여 객체의 생성자를 검사하는 방법입니다. instanceof를 사용하면 객체의 생성자를 확인하고 해당 생성자에 따라 코드 블록을 실행하거나 다른 작업을 수행할 수 있습니다.

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

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark();
  } else if (animal instanceof Cat) {
    animal.meow();
  } else {
    console.log("Unknown animal.");
  }
}

makeSound(new Dog()); // Output: Woof!
makeSound(new Cat()); // Output: Meow!
makeSound({}); // Output: Unknown animal.

위의 예제에서 makeSound 함수는 Dog 또는 Cat 클래스의 인스턴스를 받습니다. instanceof를 사용하여 객체의 생성자를 검사하고, 해당하는 생성자에 따라 다른 작업을 수행합니다.

in 키워드와 타입 가드 (in Keyword and Type Guard): in 키워드와 함께 사용하는 타입 가드는 객체의 프로퍼티 존재 여부를 검사하는 방법입니다. in 키워드를 사용하여 객체의 프로퍼티가 있는지 확인하고, 해당하는 프로퍼티가 있을 경우 해당 타입을 추론할 수 있습니다.

interface Square {
  sideLength: number;
}

interface Circle {
  radius: number;
}

function printShape(shape: Square | Circle): void {
  if ("sideLength" in shape) {
    console.log("Shape is a square.");
  } else if ("radius" in shape) {
    console.log("Shape is a circle.");
  } else {
    console.log("Unknown shape.");
  }
}

printShape({ sideLength: 5 }); // Output: Shape is a square.
printShape({ radius: 3 }); // Output: Shape is a circle.
printShape({}); // Output: Unknown shape.

위의 예제에서 printShape 함수는 Square 또는 Circle 타입의 객체를 받습니다. in 키워드를 사용하여 객체에 sideLength 프로퍼티 또는 radius 프로퍼티가 있는지 확인하고, 해당하는 프로퍼티에 따라 다른 작업을 수행합니다.

조건부 타입 (Conditional Types): 조건부 타입은 TypeScript에서 타입을 조건에 따라 결정하는 기능입니다. T extends U ? X : Y와 같은 형태로 사용됩니다. 조건부 타입은 주로 infer 키워드와 함께 사용하여 타입을 추론하는데 활용됩니다.

type Check<T> = T extends string ? true : false;

let result1: Check<number> = false;
let result2: Check<string> = true;

console.log(result1); // Output: false
console.log(result2); // Output: true

위의 예제에서 Check<T>는 타입 매개변수 Tstring 타입을 상속받으면 true를, 그렇지 않으면 false를 반환하는 조건부 타입입니다. result1result2 변수는 각각 Check<number>Check<string>를 할당받았으며, 타입 추론에 따라 falsetrue가 할당됩니다.

조건부 타입은 복잡한 타입 로직을 구현할 때 유용하며, 유니온 타입과 조합하여 유연하고 강력한 타입 시스템을 만드는데 도움이 됩니다.

모듈과 네임스페이스

모듈화, export와 import: 모듈화는 코드를 여러 파일로 분리하고, 필요한 부분을 외부로 노출(export)하고 가져와서(import) 재사용할 수 있게 하는 개념입니다. 모듈은 파일 단위로 캡슐화되어 있으며, export 키워드를 사용하여 특정 변수, 함수, 클래스 등을 외부로 노출합니다. 노출된 모듈은 import 키워드를 사용하여 다른 파일에서 가져와서 사용할 수 있습니다.

해당 부분은 JavaScript의 모듈시스템과 일치합니다. 쉽게 요약하면, TypeScript 프로젝트에서 별도의 설정이 없다면 CommonJS(require , exports.module )방식이 아닌, ESM모듈(import, export) 방식을 default로 사용한다고 보시면 됩니다.

// math.ts (모듈을 정의하는 파일)
export const add = (a: number, b: number): number => a + b;
export const subtract = (a: number, b: number): number => a - b;


// main.ts (모듈을 사용하는 파일)
import { add, subtract } from "./math";

console.log(add(5, 3)); // Output: 8
console.log(subtract(10, 5)); // Output: 5

위의 예제에서 math.ts 파일은 addsubtract 함수를 외부로 노출하고(export), main.ts 파일에서 해당 함수들을 가져와서(import) 사용하고 있습니다.

네임스페이스 (Namespace): 네임스페이스는 TypeScript에서 코드를 논리적으로 그룹화하고 충돌을 방지하는 기능입니다. 네임스페이스는 전역 범위에서 변수 이름이나 함수 이름 등이 중복되지 않도록 하기 위해 사용됩니다. namespace 키워드를 사용하여 네임스페이스를 정의할 수 있습니다.

// shapes.ts (네임스페이스를 정의하는 파일)
namespace Shapes {
  export const PI = 3.14;

  export interface Circle {
    radius: number;
  }

  export function calculateArea(circle: Circle): number {
    return PI * circle.radius * circle.radius;
  }
}

// main.ts (네임스페이스를 사용하는 파일)
import { Shapes } from "./shapes";

const circle: Shapes.Circle = { radius: 5 };
console.log(Shapes.calculateArea(circle)); // Output: 78.5
console.log(Shapes.PI); // Output: 3.14

위의 예제에서 shapes.ts 파일은 Shapes 네임스페이스를 정의하고, PI, Circle, calculateAreaShapes 네임스페이스 내부에서 정의하고 외부로 노출합니다. main.ts 파일에서는 Shapes 네임스페이스를 가져와서(import) 해당 네임스페이스의 멤버들을 사용하고 있습니다.


모듈과 네임스페이스는 코드를 구조화하고 관리하는데 유용한 기능으로, 복잡한 프로젝트에서 코드의 유지보수와 재사용성을 높이는데 도움이 됩니다. 하지만 네임스페이스는 코드의 길이를 늘리고 복잡성을 증가시킬 수 있으므로, 적절한 상황에서 사용하는 것이 중요합니다. 최신 TypeScript에서는 주로 모듈을 사용하여 코드를 구성하고, 네임스페이스는 주로 라이브러리나 외부 패키지에서 사용되고 있습니다.

타입 호환성

타입 호환성(Type Compatibility)은 TypeScript에서 변수, 함수, 클래스 등의 타입을 서로 비교하여 할당이 가능한지 검사하는 기능입니다. 두 개체가 같은 형태(구조)를 갖고 있으면 타입 호환성을 가지게 됩니다.

구조적 타이핑 (Structural Typing): 구조적 타이핑은 TypeScript에서 타입 호환성을 결정할 때 객체의 구조(프로퍼티와 메서드)가 중요하다는 개념입니다. 즉, 두 개체가 같은 구조를 갖고 있다면 타입 호환성을 가지게 됩니다.

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

let person1: Person = { name: "Alice", age: 30 };
let person2 = { name: "Bob", age: 25 };

person1 = person2; // person2의 구조가 person1과 같으므로 할당이 가능

위의 예제에서 Person 인터페이스는 nameage라는 프로퍼티를 갖고 있습니다. person1Person 타입이고, person2는 구조가 동일한 { name: string, age: number } 타입입니다. 두 개체가 같은 구조를 갖고 있으므로 person2person1에 할당할 수 있습니다.

타입 단언 (Type Assertion): 타입 단언은 TypeScript에서 특정 값을 원하는 타입으로 간주하도록 지정하는 것을 말합니다. 때때로 TypeScript가 타입을 정확하게 추론하지 못하는 상황이 발생할 수 있으며, 이때 타입 단언을 사용하여 개발자가 직접 타입을 명시할 수 있습니다.

let value: any = "hello";
let length: number = (value as string).length;

console.log(length); // Output: 5

위의 예제에서 value 변수는 any 타입으로 선언되었습니다. 따라서 TypeScript는 value의 타입을 정확히 알 수 없습니다. 하지만 개발자는 value가 문자열이라는 것을 알고 있으므로 length를 사용하기 위해 타입 단언을 사용하여 valuestring으로 캐스팅하고 있습니다. 이제 length 변수는 문자열의 길이를 가지고 있으며, 타입 추론에 의해 number 타입으로 추론됩니다.

타입 단언은 가능하면 사용을 줄이고, 타입 호환성을 고려하여 안정적인 코드를 작성하는데 주의해야 합니다. 타입 단언은 TypeScript 컴파일러에게 개발자가 더 정확한 타입을 알고 있다는 정보를 제공하는 용도로 사용되어야 합니다.

유틸리티 타입

유틸리티 타입(Utility Types)은 TypeScript에서 기본으로 제공하는 타입 중 유용한 기능을 제공하는 내장 타입입니다. 이러한 유틸리티 타입은 기존 타입을 변환하거나 새로운 타입을 생성하는데 사용됩니다. 유틸리티 타입을 꼭 쓰지 않더라도 기존의 인터페이스, 제네릭 등의 기본 문법으로 충분히 타입을 변환할 수 있지만, 유틸리티 타입을 사용하면 코드를 간결하고 재사용성을 높이는데 도움을 줍니다.

Partial 타입: 타입의 모든 프로퍼티를 선택적(optional)으로 만드는 기능을 제공합니다. 기존 타입의 모든 프로퍼티를 선택적으로 만들고 싶을 때 사용합니다.

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

const partialPerson: Partial<Person> = {};

partialPerson.name = "Alice";
partialPerson.age = 30;

위의 예제에서 Person 인터페이스를 Partial<Person>으로 변환하여 partialPerson 변수에 할당하였습니다. Partial을 사용하면 partialPerson의 모든 프로퍼티가 선택적으로 만들어집니다.

Required 타입: 타입의 모든 프로퍼티를 필수적(required)으로 만드는 기능을 제공합니다. 기존 타입의 모든 프로퍼티를 필수로 만들고 싶을 때 사용합니다.

interface PartialPerson {
  name?: string;
  age?: number;
}

const requiredPerson: Required<PartialPerson> = {
  name: "Alice",
  age: 30,
};

위의 예제에서 PartialPerson 인터페이스를 Required<PartialPerson>으로 변환하여 requiredPerson 변수에 할당하였습니다. Required를 사용하면 requiredPerson의 모든 프로퍼티가 필수적으로 만들어집니다.

Readonly 타입: 타입의 모든 프로퍼티를 읽기 전용(readonly)으로 만드는 기능을 제공합니다. 기존 타입의 모든 프로퍼티를 변경할 수 없도록 만들고 싶을 때 사용합니다.

interface Book {
  title: string;
  author: string;
}

const readOnlyBook: Readonly<Book> = {
  title: "TypeScript in Action",
  author: "John Doe",
};

// 아래 코드는 에러가 발생합니다.
readOnlyBook.title = "New Title";
readOnlyBook.author = "New Author";

위의 예제에서 Book 인터페이스를 Readonly<Book>으로 변환하여 readOnlyBook 변수에 할당하였습니다. Readonly를 사용하면 readOnlyBook의 모든 프로퍼티가 읽기 전용으로 만들어집니다.

Pick 타입: 기존 타입에서 원하는 프로퍼티만 선택하여 새로운 타입을 생성하는 기능을 제공합니다. 원하는 프로퍼티만 포함하는 새로운 타입을 만들고 싶을 때 사용합니다.

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type ProductSummary = Pick<Product, "name" | "price">;

const product: ProductSummary = {
  name: "Widget",
  price: 29.99,
};

위의 예제에서 Product 인터페이스에서 nameprice 프로퍼티만 선택하여 ProductSummary 타입으로 정의하였습니다. Pick을 사용하면 ProductSummary 타입은 Product 타입의 일부 프로퍼티만 가지고 있습니다.

Omit 타입: 기존 타입에서 원하는 프로퍼티를 제외하여 새로운 타입을 생성하는 기능을 제공합니다. 원하지 않는 프로퍼티를 제외한 새로운 타입을 만들고 싶을 때 사용합니다.

interface Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
}

type TaskWithoutId = Omit<Task, "id">;

const task: TaskWithoutId = {
  title: "Write a report",
  description: "Write a report on the project",
  completed: false,
};

위의 예제에서 Task 인터페이스에서 id 프로퍼티를 제외하여 TaskWithoutId 타입으로 정의하였습니다. Omit을 사용하면 TaskWithoutId 타입은 Task 타입의 id 프로퍼티를 제외한 나머지 프로퍼티를 가지고 있습니다.

그 외 타입: 위에서 자주 사용되는 타입들을 위주로 설명드렸으며, 소개드린 타입 이외에도 아래와 같은 유틸리티 타입들이 있습니다.

  1. Record<K, T>: 키 K와 값 T의 쌍으로 이루어진 객체 타입을 생성하는 타입.
  2. Exclude<T, U>: 타입 T에서 타입 U에 할당할 수 있는 타입을 제외한 타입을 생성하는 타입.
  3. Extract<T, U>: 타입 T에서 타입 U에 할당할 수 있는 타입을 추출하는 타입.
  4. NonNullable<T>: 타입 T에서 nullundefined를 제외한 타입을 생성하는 타입.
  5. Parameters<T>: 함수 타입 T의 매개변수 타입들을 튜플로 변환하는 타입.
  6. ReturnType<T>: 함수 타입 T의 반환 타입을 추출하는 타입.
  7. ConstructorParameters<T>: 클래스 타입 T의 생성자 매개변수 타입들을 튜플로 변환하는 타입.
  8. InstanceType<T>: 클래스 타입 T의 인스턴스 타입을 추출하는 타입.
  9. RequiredKeys<T>: 타입 T에서 모든 필수 프로퍼티의 키들을 유니온 타입으로 추출하는 타입.
  10. OptionalKeys<T>: 타입 T에서 모든 선택적 프로퍼티의 키들을 유니온 타입으로 추출하는 타입.
  11. ThisParameterType<T>: 함수 타입 Tthis 매개변수 타입을 추출하는 타입.
  12. OmitThisParameter<T>: 함수 타입 T에서 this 매개변수를 제외하는 타입.
  13. ThisType<T>: this 타입이 T인 객체를 의미하는 타입.

타입 추론 및 타입 제한

타입 추론과 타입 제한 방법은 TypeScript에서 타입을 더 정확하게 추론하고 제한하는데 사용되는 기능입니다.

infer: 조건부 타입(conditional types)에서 사용되는 키워드로, 타입 매개변수를 추론하는데 사용됩니다. infer를 사용하면 조건부 타입 내부에서 타입 추론을 수행하여 다른 타입으로 사용할 수 있습니다.

type ExtractArrayType<T> = T extends (infer U)[] ? U : never;

type Arr = string[];
type ExtractedType = ExtractArrayType<Arr>; // 추론된 타입은 string이 됩니다.

위의 예제에서 ExtractArrayType<T>T가 배열 타입인지 확인하고, 배열의 원소 타입을 infer 키워드를 사용하여 추론합니다. ExtractedTypestring 타입으로 추론됩니다.

keyof: keyof는 인터페이스 또는 객체 타입의 모든 키(key)를 유니온 타입으로 추출하는데 사용됩니다.

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

type PersonKeys = keyof Person; // 'name' | 'age'

위의 예제에서 PersonKeys 타입은 Person 인터페이스의 모든 키를 유니온 타입으로 추출하여 'name' | 'age'가 됩니다.

extends: extends는 제네릭 타입이나 조건부 타입에서 사용되어 타입 제한을 설정하는데 사용됩니다. extends를 사용하여 특정 타입을 상속받거나 제한할 수 있습니다.

type LengthLimit<T extends string> = T extends `${infer U}${infer Rest}` ? U : never;

type LimitedString = LengthLimit<"hello">; // 'h'

위의 예제에서 LengthLimit<T>Tstring 타입을 상속받는지 확인하고, 문자열의 첫 번째 글자를 추출합니다. LimitedString'h'로 추론됩니다.

조건부 타입 (Conditional Types): 조건부 타입은 extends 키워드와 함께 사용하여 타입을 조건에 따라 결정하는 기능입니다. T extends U ? X : Y와 같이 사용됩니다. 조건부 타입은 주로 infer 키워드와 함께 사용하여 타입을 추론하는데 활용됩니다.

type Check<T> = T extends string ? true : false;

let result1: Check<number> = false;
let result2: Check<string> = true;

console.log(result1); // Output: false
console.log(result2); // Output: true

위의 예제에서 Check<T>는 타입 매개변수 Tstring 타입을 상속받으면 true를, 그렇지 않으면 false를 반환하는 조건부 타입입니다. result1result2 변수는 각각 Check<number>Check<string>를 할당받았으며, 타입 추론에 따라 falsetrue가 할당됩니다.

조건부 타입은 복잡한 타입 로직을 구현할 때 유용하며, 유니온 타입과 조합하여 유연하고 강력한 타입 시스템을 만드는데 도움이 됩니다.


Usage - etc

tsconfig

TypeScript 컴파일러를 설정하는 파일은 tsconfig.json입니다. 이 파일은 프로젝트의 루트 디렉토리에 위치하며, TypeScript 컴파일러가 프로젝트를 어떻게 컴파일할지 설정하는데 사용됩니다. tsconfig.json 파일을 사용하면 컴파일러 옵션, 파일 포함 및 제외, 경로 별칭 등을 설정할 수 있습니다. tsconfig.json 파일은 기본적으로 다음과 같은 형태를 가집니다.

{
  "compilerOptions": {
    /* 컴파일러 옵션들 */
  },
  "include": [
    /* 컴파일 대상 파일들의 경로 패턴 */
  ],
  "exclude": [
    /* 컴파일에서 제외할 파일들의 경로 패턴 */
  ],
  "references": [
  	/* 참조할 프로젝트들의 경로들 */
  ]
}

"compilerOptions" (컴파일러 옵션): compilerOptions는 TypeScript 컴파일러가 코드를 컴파일할 때 사용되는 다양한 옵션들을 설정하는 부분입니다. 주요 컴파일러 옵션들을 살펴봅시다:

  • "target": 컴파일된 JavaScript 코드의 ECMAScript 버전을 지정합니다. 예를 들어 "ES5", "ES6", "ES2017" 등을 사용할 수 있습니다.
  • "module": 사용할 모듈 시스템을 설정합니다. "commonjs", "amd", "es6" 등을 사용할 수 있습니다.
  • "outDir": 컴파일된 JavaScript 파일이 생성될 디렉토리를 지정합니다.
  • "strict": 엄격한 타입 체크를 활성화합니다. "strict": true로 설정하면 타입 관련 경고와 오류가 더 엄격하게 검출됩니다.

예를 들어, 다음과 같이 "compilerOptions"를 설정할 수 있습니다.

{
  "compilerOptions": {
    /* JavaScript의 ECMAScript 버전을 설정합니다. 기본값은 "ES3"입니다. */
    "target": "ES6",

    /* 컴파일된 JavaScript 파일이 생성될 디렉토리를 지정합니다. */
    "outDir": "dist",

    /* 컴파일러가 생성하는 파일의 모듈 시스템을 설정합니다. 기본값은 "commonjs"입니다. */
    "module": "ESNext",

    /* ES 모듈을 사용하는 TypeScript 파일의 확장자를 지정합니다. 기본값은 ".ts"입니다. */
    "moduleResolution": "Node",

    /* 컴파일 시 자동으로 모든 선언 파일을 가져올지 여부를 설정합니다. */
    "types": ["node", "jest"],

    /* 타입 체크를 활성화합니다. */
    "noImplicitAny": true,

    /* 정적 검사를 강화하는 여러 옵션들을 함께 활성화합니다. */
    "strict": true,

    /* JSX를 사용할 때 React 코드를 컴파일하도록 설정합니다. */
    "jsx": "react",

    /* strictNullChecks 옵션을 활성화하여 null과 undefined를 따로 다룹니다. */
    "strictNullChecks": true
  },
  //...중략
}

"include"와 "exclude":

  • "include": 컴파일 대상으로 포함할 파일 또는 디렉토리의 경로 패턴을 설정합니다. 일반적으로 "include"를 설정하지 않으면 프로젝트 루트 디렉토리부터 모든 TypeScript 파일을 컴파일합니다.
  • "exclude": 컴파일에서 제외할 파일 또는 디렉토리의 경로 패턴을 설정합니다. "node_modules"와 같이 컴파일하지 않아야 할 디렉토리를 지정하는데 사용됩니다.

예를 들어, 다음과 같이 "include""exclude"를 설정할 수 있습니다.

{
  //...중략
  "include": ["src/**/*.ts"], // src 디렉토리 내의 모든 .ts 파일을 컴파일 대상으로 지정
  "exclude": ["node_modules", "build"] // node_modules와 build 디렉토리는 컴파일에서 제외
}

다중 프로젝트 설정: 만약 여러 개의 TypeScript 프로젝트가 하나의 루트 디렉토리에서 관리되는 경우, 다중 프로젝트 설정을 사용할 수 있습니다. references 옵션을 사용하여 서로 다른 프로젝트 간의 의존성을 정의할 수 있습니다.

예를 들어, 다음과 같이 references를 설정할 수 있습니다.

{
  "compilerOptions": {
    /* 공통 컴파일러 옵션들 */
  },
  "references": [
    { "path": "./shared" },
    { "path": "./app" }
  ]
}

위의 예제에서 sharedapp 프로젝트 간에 의존성을 정의했습니다. 이렇게 하면 shared 프로젝트를 먼저 컴파일하고, 그 후 app 프로젝트를 컴파일하여 모든 의존성을 만족할 수 있게 됩니다.


tsconfig.json 파일을 적절하게 설정하면 TypeScript 컴파일러가 프로젝트를 원하는 방식으로 컴파일할 수 있습니다. 또한, 프로젝트의 구조와 요구 사항에 맞게 컴파일러 옵션과 파일 포함/제외를 설정하여 타입 안정성과 성능을 최적화할 수 있습니다.


마치며


객체지향 언어를 문법적으로 잘 사용한다고 해서 객체지향적인 프로그래밍을 잘 할 수 있는 게 아니듯이, 타입스크립트 문법을 다 안다고 해서 유지보수하기 좋은 프로그래밍을 하는 것은 아니라고 생각합니다.

언어의 문법과 기능을 이해하는 것은 중요하지만, 그것만으로는 좋은 프로그래밍을 보장하지 않습니다. 프로그래밍은 문제 해결과 기능 구현에 초점을 맞추어야 하며, 해당 글은 구현 과정에 문법적인 빠른 참조를 위해 사용법을 주로 작성되었습니다. 따라서, 깊게 학습하고 싶으신 분들은 공식 문서와 좋은 학습 자료를 참고하여 언어의 기능을 깊게 이해하고 실전에서 적절히 활용하셨으면 좋겠습니다.


참고

profile
내일 더 효율적이고 싶은 개발자

8개의 댓글

comment-user-thumbnail
2023년 8월 6일

굉장히 길고 알찬 포스팅이네요~ 잘 보고 갑니다~

2개의 답글
comment-user-thumbnail
2023년 8월 6일

좋은 글 감사합니다. 자주 방문할게요 :)

답글 달기
comment-user-thumbnail
2023년 8월 6일

TS를 항상 어떻게 시작할지 고민이었는데, 좋은 가이드라인이 될 것 같습니다. 감사합니다 😻

1개의 답글
comment-user-thumbnail
2023년 8월 7일

타입스크립트 배우기에 좋은 글이네요 감사합니다

1개의 답글