TypeScript

이찬영·2024년 11월 8일

TypeScript 환경세팅

node.js프로젝트 환경세팅
npm init => npm install --save-dev typescript => npx tsc --init(타입스크립트를 사용할 때 필요한 설정파일 만드는 커맨드)

package.json파일에 scripts필드에 "build" : "tsc" (이제 npm run build로 ts파일을 js로 변환해줄 수 있다.)

예시로, main.ts파일을 만들고, console.log('hello typescript');를 작성한 뒤 npm run build를 하면, main.js파일이 생성된다. 그럼 우리가 작성한 적 없는 "use strict"; 라는 코드가 main.js파일에 들어가 있는데, 이것은 자바스크립트 코드를 strict 모드로 실행할때 사용한다. 쉽게 말해, 좀 더 엄격한 규칙으로 자바스크립트를 실행하고 싶을때 사용한다.

tsconfig.json파일에 보면 "strict"라는 필드가 true옵션으로 켜져있는데, 이 옵션을 켜면 always strict라는 옵션이 켜져서 use strict를 추가해주게 된것이다.

타입 스크립트가 실행되는 과정

타입스크립트는 기존 자바스크립트에 문법을 추가해서 사용하는 언어이다. (슈퍼셋이라고 부름)
타입스크립트 컴파일러(tsc)는 타입스크립트 코드를 자바스크립트 코드로 트랜스파일 해주는 프로그램이다. 타입 스크립트는 코드에 잘못된 부분이 있는지 체크한뒤, 그 잘못된 부분이 해결되면, 컴파일한다.

타입을 정하는 법

# main.ts
let size = 100;
size = 'L';

이렇게 하면 자바스크립트땐, 그냥 값이 덮어씌워지겠지만, 타입스크립트에서는 오류가 나게 된다.
타입스크립트는 이렇게 추론을 하지않고, 확실히 적어줄 수 있다.

let size : number = 100;

변수명 앞에 콜론(:)을 붙이고, 타입을 정해줄 수 있다.

곧바로 값을 할당하지 않을때는,

let size : number;

이렇게 타입만 적어주면 된다.

기본형

let itemName : string = "후드티"; // 문자열 = string
let itemPrice : number = 123000; // 숫자형 = number
let membersOnly : boolean = true;  // 불린형 = boolean
let owner : undefined = undefined;  // undefined
let seller : null = null;  // null
// 참고로 null과 undefined는 서로 다른 값이다.

배열과 튜플

문자열 배열을 만들고 싶으면, 타입을 먼저 쓰고 옆에 빈배열을 써주면 된다.

const cart : string[];
// 배열의 배열
const cart : string[][];

갯수를 정확히 맞추고 싶을땐 튜플타입을 쓰면 된다.
튜플은

let mySize : [number, number] = [123, 32];
mySize = [123, 32, 44]; // 에러
mySize = [255]; // 에러
mySize = [];  // 에러
// 튜플은 꼭 같은 타입으로 넣을 필요 없다.
let mySize : [number, number, string] = [123, 32, 'L'];

객체타입

let product : {
	id = string;
	name : string;
	price : number;
	membersOnly? : boolean; // ?는 옵셔널할때 사용
    sizes : string[];
} = {
	id : 'c001',
  	name : '후드티',
  	price : 123000,
  	sizes : ['M', 'S'];
};

if(membersOnly) {
	console.log('회원 전용 상품');
}else {
	console.log('일반 상품');
};

또 특이한 문법이 있는데
자바스크립트 문법중에서, 객체에 값을 할당할 때, 프로퍼티 이름에 변수를 쓰고 싶으면,

let field = 'field name';
let obj = {
	[field] = 'field value';
};

이렇게 썼었는데, 타입스크립트에도 비슷한 문법이 있다.
프로퍼티 갯수를 정하지않고, 프로퍼티 값의 타입을 정하고 싶을때 아래의 문법을 사용하면 된다.

let stock : {[id : string] : number} = {
	'c001' : 3,
  	'c002' : 1,
  	'c003' : 0
};

이렇게 하면 객체의 키는 string, 밸류는 number인 객체를 만들 수 있다.

any

타입을 any로 함으로써, 타입오류에서 자유롭게 만들 수 있는 타입이다. 되도록 쓰지않는것이 좋지만, 꼭 써야한다면 as라는 문법을 붙여서 타입을 적어주거나 콜론(:)을 써서 타입을 명시적으로 지정하면 된다.

const product : any = {
	id : 'c001',
  	name : '후드티',
  	price : 123000,
  	size : ['M', 'S']
};

const parsedProduct = JSON.parse(
  `{"name" : "토트백", "price" : "12000"}`
) as {
	name : string;
  	price : number;
};

함수에 타입 정의하기

  1. 함수 선언에서 타입을 정하기
const stock : {[id : string] : number} = {
	'c001' : 3,
  	'd001' : 4,
};
const cart : string[] = [];

function addToCart(id : string, quantity : number = 1) : boolean {
	if(stock[id] < quantity){
    	return false;
    }
  
  	stock[id] -= quantity;
  	for(let i = 0; i < quantity; i++){
    	cart.push(id);
    }
  return true;
}
  1. 함수 자체에 타입을 정하기
const shop : {
	stock : {[id : string], number};
  	cart : string[];
  	addToCart : (id : string, quantity : number = 1) => boolean
  	//먼저 파라미터의 타입을 명시해주고, return값의 타입은 화살표(=>)하고 적어주면 된다.(:도 가능)
} = {
	stock : {
    	'c001' : 3,
      	'c002' : 1,
    },
  cart : [],
  addToCart,
};
  • 아무런 값도 리턴하지 않는 함수가 있다면, void를 적어주면 된다.

Enum

enum Size {
	S = 'S',
  	M = 'M',
  	L = 'L',
  	XL = 'XL',
}

let product : {
	id : string;
  	name : string;
  	price : number;
  	sizes : Size[];
} = {
	id : 'c001',
  	name : '후드티',
  	price : 123000,
  	sizes : [Size.S, Size.L],
};

이렇게 string[]이라는 광범위한 값을 쓰는것이 아닌, enum으로 들어올 수 있는 값을 정해놓을 수 있다.

interface

interface를 사용하는 방법은 interface를 쓴 다음, 객체타입처럼 만들면 된다.

enum Size {
	S = 'S',
  	M = 'M',
  	L = 'L',
  	XL = 'XL',	
}

interface Product {
	id : string;
  	name : string;
  	price : number;
  	membersOnly? : boolean;
}

interface를 상속하고 싶으면 interface 이름 뒤에 extends를 적은 다음 부모 interface이름을 적어주면 된다. (자식이 부모의 필드를사용할 순 있지만, 부모가 자식의 필드를 사용할 수는 없다.)

interface ClothingProduct extends Product {
// ClothingProduct는 Product의 모든 필드를 상속받기 때문에 모두 사용할 수 있다.
  sizes : Size[];
}

const product1 : ClothingProduct = {
	id : 'c001',
  	name : '후드집업',
  	price : 123000,
  	sizes : [Size.M, Size.L],
};

const product2 : Product = {
	id : 'd001',
  	name : '후드티',
  	price : 30000
}

리터럴 타입

타입스크립트에선 let을 쓰느냐 const를 쓰느냐에 따라서 타입이 다르게 추론된다.
let small = 95; // 타입은 number
const large = 100; // 타입은 100
즉, 리터럴타입은 '값을 곧 타입'으로 하는 타입이다.

타입별칭

타입 스크립트에서는 타입도 변수처럼 이름을 붙여줄 수 있는데, 이를 타입별칭, 영어로는 Type Aliase라고 한다.

type Cart = string[];

const cart : Cart = [
	'c001',
  	'c002',
  	'c003'
]; // cart : string[]이 아닌 Cart라는 별칭 즉, Cart = string[]이라는 타입이 Cart라는 타입이 된것이다.

interface User {
	username : string;
  	email : string;
  	cart : Cart;  // 여기서도 cart : string[]이 아닌, type으로 선언해준 Cart를 넣을 수 있다.
}

쉽게 예시를 들어보면,

 type Nums = [number, number]; //라는 타입을 만들면
function getDiff(fromPoint : Num, toPoint: Num) : boolean {...} 
//파라미터를 보듯, 어디든 새로운 타입으로 적용시킬 수 있다.

Union타입

Union타입은 하나의 변수가 여러가지 타입중 하나를 가질 수 있도록 하는 기능이다.
쉽게 말해 enum과 비슷한데, "이 변수는 이 타입이거나 저 타입일 수 있다."고 지정하는 방법이다.

|(파이프)기호로 여러 타입을 연결하여 정의한다.

  1. 기본사용법
    Union타입을 사용하면 변수가 여러 타입을 가질 수 있다. 예를 들어, number 또는 string타입일 수 있는 변수를 정의해보면
let id : number | string;

id = 123; //number할당가능
id = 'abc' //string할당가능

# 함수에서도 사용이 가능하다.
# interface로 Product를 상속받은 ClothingProduct와 ShoeProduct가 정의되어 있을때

function printSize(product : ClothingProduct | ShoeProduct) {
	const availableSizes = product.sizes.join(', ');
}
  1. Union타입을 사용하는 이유
    더 유연하게 여러 타입을 처리할 수 있어 다양한 상황에 적응할 수 있는 코드를 작성할 수 있다. 예를 들어, 함수 매개변수가 문자열이나 숫자를 모두 받는 경우 Union타입이 유용하다.
function printId(id : number | string) {
	console.log(id);
};

printId(101);  //number타입
printId('abc'); //string타입
  1. Union타입과 조건부 코드
    Union타입을 사용할때는 타입에 따라 다르게 처리해야 하는 경우가 종종 있다. 이때 타입 검사를 통해 조건부 코드를 작성할 수 있다.
function printId(id : number | string) {
	if(typeof id === 'string'){
    	console.log('string일때 처리');
    }else {
    	console.log('number일때 처리')
    }
}
  1. 다양한 상황에 사용되는 Union타입
    실제로 유용하게 사용되는 예시가 있다. string또는 number타입으로 사용자의 나이를 저장하는 예인데, string을 해주지않고, 바로 "unknown"처럼 할 수도 있다.
type Age = number | "unknown";

let userAge : Age;

userAge = 25; //number타입으로 저장
userAge = 'unknown' // string타입으로 저장

정리하자면,

  • Union타입은 여러 타입 중 하나가 될 수 있는 변수를 정의할 때 사용한다.
  • | 기호로 여러 타입을 연결한다.
  • 타입 검사를 통해 각 타입에 맞는 동작을 구현할 수 있다.

[트러블슈팅💣]

if문에서 in문법을 사용할때는, '속성명' in 객체를 적어야 한다.

intersection 타입

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

interface Worker {
	company : string;
  	jobTitle : string;
}

// intersection 타입 정의
type PersonWorker = Person & Worker & {
	email : string;
};

const employee : PersonWorker = {
	name : "alice",
  	age : 30,
  	company : 'any',
  	jobTitle : 'Engineer',
  	email : 'example@naver.com'
}

intersection과 Union

intersection은 연산자 &과 쓰인것 모두의 성질을 함께 갖고 있는것이어야 하고, Union은 | 기호에 쓰인것이 포함되면 되는것이다.

keyof연산자와 typeof연산자

keyof 연산자

keyof연산자는 객체 타입의 키들을 타입으로 반환한다. 객체 타입에 있는 키를 기반으로 타입을 정의할 때 유용하다.

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

// keyof Person을 하게 되면, 'name' | 'age'와 같은 유니언 타입을 반환한다.
type PersonKeys = keyof Person; // 'name' | 'age'

//여기서 PersonKeys타입은 'name'또는 'age'라는 문자열만 가질 수 있다. keyof를 사용하면 객체의 
// 다른 키를 다른 타입 정의에 활용할 수 있어, 유연하고 타입 안전한 코드를 작성하는데 유리하다.

typeof연산자

typeof연산자는 자바스크립트의 typeof와 비슷하게 사용되지만, 타입스크립트에서는 값을 기반으로 새로운 타입을 생성할 때 사용하는것이 차이점이다. 타입스크립트의 typeof는 변수나 객체, 함수의 타입을 가져오는데 사용되며, 이미 선언된 변수를 타입으로 다시 정의할 때 유용하다.

  1. 변수타입을 그대로 가져오기
    일반적으로 변수의 타입을 알고 싶다면, typeof를 사용하면 된다.
const age = 30;

type AgeType = typeof age; // number

위 예제에서 typeof age는 age변수가 가지고 있는 타입을 반환한다. age가 number타입이기 때문에 AgeType도 number타입이 된다. 즉, 변수의 타입을 그대로 다른곳에 정의하고 싶을때 typeof를 사용한다.

  1. 객체의 타입 가져오기
    객체의 타입도 typeof로 가져올 수 있다.
const person = {
	name : 'alice',
  	age : 25,
};

//person객체의 타입을 그대로 가져온다.
type PersonType = typeof person; // {name : string, age : number}

위 코드에서 typeof person은 person객체의 타입인 {name : string, age : number}를 그대로 가져와 PersonType이라는 새로운 타입을 만든다. 이렇게 하면 객체 구조를 복사하는것처럼 객체의 타입을 쉽게 재사용 할 수 있다.

type 별칭과 interface와 Enum

interface와 Enum, 타입별칭은 거의 같은 역할을하는 코드이다.
타입별칭과 Union을 사용하는것보단, Enum의 목적에 맞게 사용할 수 있다면, Enum을 쓰는게 권장되고, 타입별칭보단 interface, extends문법을 사용하는것이 권장된다.

정리

리터럴 타입

  • 특정한 숫자나 문자열같이 변수의 값을 타입으로 하는 타입이다. 각 리터럴 타입들은 string이나 number같은 더 큰 타입에 포함된다.
const name = 'code'; //'code'라는 리터럴 타입
const rank = 1; // 1이라는 리터럴 타입

타입 별칭
복잡한 타입에 이름을 붙이고, 재사용하고 싶을대 사용한다.

  • type Point = [number, number];
  • type SearchQuery = string | string[];
  • type Result = SuccessResult | FailedResult;
  • type Coupon = aCoupon | bCoupon | cCoupon...

Union
A이거나 또는 B인 경우를 타입으로 만들고 싶을때

  • ClothingProduct | ShoeProduct

Intersection
A와 B의 성질 모두 갖는 타입을 만들고 싶을때

interface Entity {
	id : stirng
  	createdAt : Date;
  	updatedAt : Date;
}
type Product = Entity & {
	name : string;
  	price : number;
  	membersOnly? : boolean;
}

하지만 보통 이럴때는 interface와 상속을 사용하는것을 권장한다.

interface Entity {
	id : string;
  	createdAt : Date;
  	updatedAt : Date;
}
interface Product extends Entity {
	name : string;
  	price : number;
  	membersOnly? : boolean;
}

keyof 연산자
객체타입에서 프로퍼티 이름들을 모아서, Union한 타입으로 만들고 싶을때 사용한다.

interface Product {
	id : string;
  	name : string;
  	price : number;
  	membersOnly? : boolean;
}
type ProductProperty = keyof Product; // 'id' | 'name' | 'price' | 'membersOnly'

typeof 연산자
자바스크립트 코드에서 사용하면 결괏값이 문자열이지만, 타입스크립트 코드에서 쓸때는 결과값을 타입스크립트의 타입이다.

const product : Product = {
	id : 'c001',
  	name : '후드',
  	price : 123000,
  	salePrice : 98440,
  	membersOnly : true
};
console.log(typeof product); // 문자열 'object'
const product2 : typeof product = { //타입스크립트의 Product타입
	id : 'd001',
  	name : '텀블러',
  	price : 30000,
  	salePrice : 19000,
  	membersOnly : false,
}

제네릭(Generic)

제네릭은 데이터 타입을 특정하지 않고, 코드가 다양한 타입과 함께 작동하도록 만드는 방법이다.
제네릭을 사용하면 함수나 클래스, 인터페이스에서 다양한 타입을 지원하면서도 타입안정성을 유지할 수 있다.
쉽게 말해, 제네릭은 코드 작성시 특정 타입에 얽매이지 않도록 해주는, 일종의 "타입 변수"라고 생각할 수 있다.

  1. 제네릭 없이 사용하는 경우
    먼저 제네릭이 없을때의 예제를 살펴보자. 예를들어, 어떤 값을 받아 그대로 반환하는 함수가 있다고 했을때
function identity(value : any) : any {
	return value;
}

이 함수는 어떤 타입의 value든지 받을 수 있지만, 반환 타입 역시 any이기 때문에 타입 안전성이 떨어진다. 예를 들어, 문자열을 입력해도 타입스크립트는 반환되는 값이 문자열인지 확인할 방법이 없다.

  1. 제네릭으로 개선하기
    위 문제를 해결하기 위해 제네릭을 사용해보자. 제네릭을 사용하면 입력된 타입을 그대로 반환할 수 있다.
function identity<T>(value : T) : T {
	return value;
}

여기서 <T>는 타입변수로, 함수가 호출될때 전달된 값의 타입으로 설정된다. 이제 identity함수는 어떤 타입이든 받아서 그대로 반환하면서, 입력된 타입이 자동으로 반환 타입에도 적용된다.

let result1 = identity<string>('hello') // result1은 string타입으로 추론 + 명시적으로 제네릭타입을 <string>으로 지정
let result2 = identity(3)  // result2는 number타입으로 추론
  1. 제네릭을 사용한 배열 예제
function getFirstElement<T>(arr : T[]) : T {
	return arr[0];
}

let firstString = getFirstElement(["apple", "banana", "cherry"]); // string타입으로 추론
let firstNumber = getFirstElement([1, 3, 5]); // number타입으로 추론

여기서 T[]는 제네릭 타입 T를 요소로 가지는 배열이라는 뜻이다. getFirstElement함수는 입력배열의 타입에 따라 반환 타입이 자동으로 추론된다. + arr배열에 number와 string이 혼합되어 있어도, T는 타입을 string | number로 자동추론된다.

  1. 제네릭 인터페이스
    제네릭은 인터페이스에도 사용할 수 있다. 예를들어, Response라는 제네릭 인터페이스를 만들어 보겠다.
interface Response<T> {
	data : T;
  	status : number;
}

const stringResponse : Response<string> = {
	data : "Hello, World!",
  	status : 200,
};
// Response<string>처럼 <>안에 타입을 명시해줘야함

const numberResponse : Response<number> = {
	data : 42,
  	status : 200,
};

요약하면, 제네릭은 코드 작성 시 타입을 고정하지않고, 다양한 타입과 함께 작동하도록 만든다.
<T>는 일반적인 제네릭 타입 변수이며, 특정 타입으로 제한하지 않는 타입변수로 사용된다.
제네릭을 사용하면 함수, 클래스, 인터페이스에서 다양한 타입을 안전하게 지원할 수 있어 코드의 재사용성이 높아진다.

모듈 사용하기

ts파일을 빌드하게 되면, js파일이 자동으로 생기는데, 보기 안좋을 수 있다.
그래서 이것을 tsconfig파일에서 설정할 수 있는데, 이건 tsconfig.json파일로 가서 "compilerOptions"안에 "outDir"옵션을 설정하면 된다. (빌드한 파일들을 모아놓을 경로를 정할 수 있다.)
만약 아무런 설정도 해놓지 않는다면, 소스코드랑 트랜스파일된 결과물이랑 같은 위치에 만들어준다.
outDir에 커서를 놓고 ctrl + /를 해준뒤, 만들 폴더명을 적어주면 된다.
그리고, compilerOptions를 접고 바깥에, "include" : [] 와 "exclude" : []를 추가해준다. 이건 타입스크립트를 처리할때 어떤 파일들을 포함할건지 어떤 파일들을 제외할건지 설정하는것이다. 배열안에는 경로 패턴을 문자열로 적어주면 된다.
예를 들어, "include" : ["/src/**/*"]을 해주면 src파일안에 있는 모든 파일을 포함하겠다는 의미이다. 이런 경로 패턴을 Glob패턴이라고 한다. 마지막으로 rootDir이라는 옵션을 살펴보자
rootDir은 기본적으로 컴파일하는 파일들의공통된 조상 폴더를 찾아서 그 경로를 기본값으로 한다.
즉, 타입스크립트 컴파일러가 컴파일할 소스파일의 기본 디렉토리를 지정하는 설정이다.
이것은 "rootDir" : "src"로 해놓고, "outDir" : "./dist"로 해주게 된다면, src폴더에서 ts파일들이 컴파일된 js파일들로 dist파일에 그대로 (디렉토리까지 포함해서) 생성된다.

modeule : 어떤 방식으로 모듈을 만들지
크게 두가지가 있다. ES6부터 도입된 import / export문법을 사용하는 ESM방식이 있고, Node.js같은데서 기본적으로 사용하는 CJS방식이 있다. 이 옵션에서는 자바스크립트 코드로 트랜스 파일할때 어떤 모듈 문법으로 변환할지 선택할 수 있다. 보통 Node.js환경에선 CJS를 사용하고 프론트엔드 개발에선 상관없이 쓴다.

0개의 댓글