타입스크립트(TS)에 대해 알아보자

mason.98·2023년 8월 8일
0

알쓸코잡

목록 보기
14/18

TypeScript(이하 TS)는 자바스크립트 기반으로 만들어진 언어이다.
자바스크립트가 갖고 있는 여러문제를 해결하고 보완하기 위해 만들어졌다.

✅ 타입 안정성

자바스크립트는 개발자가 오류가 가득한 코드를 적어도
최대한 이해하려고 노력하는 언어이다.

예를 들어보자.
배열false(bool)를 더해보자.ㅋㅋ

[1, 2, 3, 4] + false

// result
'1,2,3,4false'

말도 안되는 코드를 찰떡같이 표현한다.
지금 당장은 신기하다!에서 끝나겠지만, 나중에 에러가 떠도
어디가 문제인지 확인하기가 어려워질 수도 있을 거 같다.

이번엔 실제 상황일법한 예시를 들어보자.

// Function
const divide = (a, b) => {
	return a / b;
}
// call Function
divide("aaaa");
divide(2, 3);

// result
NaN
0.66666666

divide함수의 인자를 확인하지 않고, 일단 실행하고 값을 반환했다.
예외처리도 안되고, 에러메시지가 뜨지도 않는다.
또한 인자가 필수사항인지, 선택사항인지도 자바스크립트는 알지 못한다.

다른 프로그래밍언어는 컴파일 되기 전에 알려주지만,
자바스크립트는 이미 프로그램이 실행되고나서 해당 함수가 실행되면서
런타임에러가 발생하는 것이다.

이러한 문제들을 TS는 해결해준다-!

TS는 코드를 컴파일해서 자바스크립트로 변환되기 전에
그 코드(TS)를 확인하여 에러가 발생하면 자바스크립트로 컴파일하지 않는다.
즉, 런타임에러가 발생하기 전인 컴파일단계에서 코드가 유효한지 검사한다.


✅ 타입시스템

TS는 데이터와 변수의 타입을 자바처럼 명시적으로 정의할 수도 있고,
자바스크립트처럼 생성만 하고 넘어갈 경우 타입추론을 해준다.

let a = "test"; // string으로 타입추론
let a : boolean = false; // boolean으로 명시적으로 정의

✅ 타입별칭

타입별칭은 나중에 쉽게 참고할 수 있도록 도와준다.
?를 붙인 변수는 해당 타입이거나 undefined임을 의미한다.

interface Player = {
  name: string,
  age?: number, // number || undefined
},
  
const player1 : Player = {
	name: "mason",
}
const player2 : Player = {
	name: "mount",
  	age: 22,
}

// (화살표함수) 매개변수는 string, return타입은 Player인 함수
const setPlayer = (name: string) : Player => ({
	name,
});

// (일반함수)
function setPlayer(name: string) : Player {
	return {
    	name,
    };
}

✅ 여러가지 타입들

any

any타입은 TS로부터 빠져나오고 싶을 때 사용하는 타입이다.

unknown

변수의 타입을 미리 알지 못할 때, unknown타입을 사용한다.

void

아무것도 return하지 않는 함수에 대해 사용하는 타입이다.
보통 자동으로 설정된다.

never

함수가 절대 return하지 않을 때에 발생한다. (예외에러처리)
거의 사용하지 않는다.


✅ Call Signature

함수 위에 마우스를 올렸을 때 보게 되는 것을 의미한다.
함수의 매개변수(arg)타입과, 함수의 return 타입은 무엇인지에 대해 알려준다.

즉, 함수의 매개변수와 반환 타입을 지정하는 것을 말한다.

// call signature
type Add = (a: number, b: number) => number;

const test: Add = (a, b) => a+b;

Call Signature로 test함수의 데이터타입을 TS가 유추할 수 있게 도와주었다.

✅ 오버로딩(Overloading)

개발하다보면 많은 외부 라이브러리들을 사용하는데,
이 때 외부 라이브러리들은 오버로딩을 많이 사용하게 된다.

오버로딩은 함수가 서로 다른 여러개의 call signature를 갖고 있을 때만 발생한다.

코드를 작성하다가 아래와 같은 경우를 경험한 적이 있을 것이다.
둘 다 똑같이 "/home"이라는 경로로 이동하는 코드임을 짐작할 수 있다.

// 1. call signature
Router.push("/home");
// 2. call signature
Router.push({
	path: "/home",
  	args: { ... },
});

1번의 경우는 아무런 옵션없이 이동하는 경우일거고,
2번의 경우는 인자를 같이 포함해서 이동하는 경우로 짐작할 수 있다.

이처럼 TS로 코딩을 하다보면 위와같이 같은 함수(push)를 사용하지만
서로 다른 call signature를 갖고 있는 경우에는 각각의 call signature를 설정해주어야 한다.

// 2번째 call signature의 데이터타입
type PushConfig = {
	path: string,
  	args: object,
}

// push함수의 Call Signatures
type RouterPush = {
	(path: string) => void, // 1번째 call signature
    (config: PushConfig) => void, // 2번째 call signature 
}

// push 함수
const push:RouterPush = (config) => {
	if(typeof config === "string"){
    	console.log("1번째 call signatrue 호출");
    } else {
      	console.log("2번째 call signatrue 호출");
    }
};

// 1번째 call signature 호출
Router.push("test"); 

// 2번째 call signatrue 호출
Router.push({
	path: "test",
  	args: { ... },
}); 

✅ 다형성(Polymorphism) + 제네릭(Generic)

배열을 출력하는 함수의 call signature를 작성하려고 한다.
위에 배운 방법으로 만들어보면 아래와 같이 작성할 수 있다.

type TypePrint = {
	(arr: number[]) : void
	(arr: string[]) : void
  	(arr: boolean[]) : void
}

너무 비효율적이다.. 어떻게 하면 좀 더 효율적으로 코드를 작성할 수 있을까?


제네릭(Generic)은 변수/함수의 선언 시점이 아니라 생성 시점에 타입을 명시하여,
하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.

이럴 때, 제네릭(generic)을 사용할 수 있다.
제네릭은 데이터 타입을 자동으로 유추할 수 있게 한다.
아래는 제네릭을 사용한 call signature이다.

type TypePrint = {
	<T>(arr: T[]) : T
}

<T>: T라는 제네릭을 사용하겠다.
(arr: T[]): 매개변수는 T라는 제네릭타입으로 이루어진 배열이다.
: T : 제네릭타입으로 return 한다.

type TypePrint = {
	<T>(arr: T) : T
}

const gPrint : TypePrint = (arr) => arr[0];

const a = gPrint([1,2,3,4]);
const b = gPrint(["a","b","c","d"]);
const c = gPrint([false, "ds", 1]);

a,b,c 변수의 각 Call Signature는 아래와 같이 제네릭이 자동으로 설정한다.

type ATypePrint = {
	(arr: number[]) : number
}
type BTypePrint = {
	(arr: string[]) : string
}
type CTypePrint = {
	(arr: (number|boolean|string)[]) : (number|boolean|string)
}

a의 T는 number가 되고,
b의 T는 string,
c의 T는 number|boolean|string이 되는 것이다.

간단하게 선언해보자.

function gPrint<T>(a: T[]) {
	return a[0];
}

제네릭(Generic)은 변수/함수의 선언 시점이 아니라 생성 시점에 타입을 명시하여,
하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.


✅ 클래스 (class)

TS에서만 사용할 수 있는 기능들이다.
(추상클래스, 추상메소드, protected, private 등)

컴파일 되는 순간 자바스크립트에는 반영되지 않는다.

// Player 클래스
class Player {
  // 생성자
  constructor(
  	private firstName: string,
    private lastName: string,
    public nickname: string,
  ) {}
}

const p1 = new Player("first", "last", "nickname");
p1.firstName = "edit"; // private이므로 작동하지 않는다.
p1.nickname = "nickname"; // public이므로 가능하다.

추상클래스

  • 다른 클래스가 상속받을 수 있는 클래스이다.
  • 직접 새로운 인스턴스를 생성할 수 없다. (new Class ❌)

추상메소드

  • 직접 구현할 수 없고, 메소드의 call signature만 지정해야 한다.
  • 추상 메소드는 상속받은 클래스에서 반드시 생성되어야 한다.

public, private, protected, static

public 프로퍼티의 경우, 접근 및 값 수정이 자유롭다.
private 프로퍼티의 경우, 클래스 내부에서만 접근할 수 있다.
protected 프로퍼티의 경우, 클래스 내부와 자식클래스에서만 접근할 수 있다.
static 프로퍼티의 경우, 클래스 내에서 사용하는 함수일 때 사용한다.
클래스 인스턴스가 없어도 사용할 수 있다. (유틸리티 함수처럼 사용가능)

// 추상 클래스 User
abstract class User {
  constructor(
  	private firstName: string,
    private lastName: string,
    protected nickname: string,
  ) {}
  // 🚀 일반 메소드
  getFullName(){
  	return `${this.firstName} ${this.lastName}`;
  }
  // 🚀 추상 메소드 (call signature만 지정)
  abstract getNickname() : string
}

// ✅ User 클래스를 상속받은 Player 클래스
class Player extends User {
  	// 🚀추상 메소드 생성
	getNickname() {
    	return this.nickname;
    }
}

const u1 = new Player("choi", "hyuk", "nickname");
u1.getFullName(); // 상속받은 메소드를 사용한다. result: "choi hyuk"
u1.getNickname(); // 추상메소드 result: "nickname"

✅ 인터페이스(interface)

type은 TS에게 오브젝트/변수의 모양을 알려주는 것이다.
인터페이스는 TS에게 오브젝트의 모양만을 특정하여 알려준다.

따라서 type 키워드가 좀 더 활용할 수 있는게 많다.

// type
type Team = "red" | "blue"
type Player = {
	nickname: string,
  	team: Team,
}

// interface
interface Player {
  nickname: string,
  team: Team,
}

인터페이스의 몇가지 특징에 대해 알아보자.

1. 인터페이스(interface)끼리 상속할 수 있다.

interface Player {
  nickname: string,
  team: Team,
}

// 상속 받은 interface User
interface User extends Player {}

const user1: User = {
	nickname: "mason",
	team: "red",
}

2. 인터페이스(interface)는 클래스에게도 상속할 수 있다. (타입도 동일함)

// User 인터페이스
interface User {
    firstName: string,
    sayHello(name: string) : string
}

// User 인터페이스를 상속받은 Player 클래스
class Player implements User {
    constructor(
        public firstName: string, // public이 강제된다.
    ){}
    sayHello(name: string){
        return `hi ${name}`;
    }
}

TS에만 있는 implements 키워드를 통해서 클래스에게 상속할 수 있다.

3. 확장성에 용이하다.

여러 인터페이스를 동시에 상속받는 클래스를 생성할 수도 있다.

// interfaces
interface User {
    firstName: string,
}
interface User {
	lastName: string,
}
interface Human {
    age: number,
}


// User,Human 인터페이스를 상속받은 Player 클래스
class Player implements User, Human {
    constructor(
        public firstName: string, // User 인터페이스 상속
        public lastName: string, // User 인터페이스 상속
        public age: number, // Human 인터페이스 상속
    ){}
}

4. 코드가 더욱 가벼워진다.

추상 클래스를 상속받아 구현한 Player1 클래스와
인터페이스를 상속받아 구현한 Player2 클래스이다.

두 코드를 자바스크립트로 컴파일했을 때, 코드의 길이가 다르다.

// 추상 클래스 User1
abstract class User1 {
    constructor(
        protected firstName : string,
        protected lastName : string,
    ){}
    // 추상 메소드
    abstract sayHello(name: string) : string;
    abstract fullName() : string;
}
// 추상클래스 User1를 상속받은 Player1
class Player1 extends User1 {
    sayHello(name: string){
        return `Hello ${name}!`;
    }
    fullName() {
        return `${this.firstName} + ${this.lastName}`;
    }
}

// ==========================================

// 인터페이스 User2 
interface User2 {
    firstName: string,
    lastName: string,
    sayHello(name: string) : string,
    fullName() : string,
}
// 인터페이스 User2를 상속받은 Player2 
class Player2 implements User2 {
    constructor(
        public firstName: string,
        public lastName: string,
    ){}
    sayHello(name: string){
        return `Hello ${name}!`;
    }
    fullName() {
        return `${this.firstName} + ${this.lastName}`;
    }
}

자바스크립트에선 interface/implements라는 개념이 없기 때문에
많은 부분이 생략되어 인터페이스를 사용했을 때, 코드가 더욱 가벼워진다.

따라서 추상클래스를 다른 클래스의 정의를 위해만 사용한다면
같은 역할을 하면서 코드가 가벼운 인터페이스를 사용하는게 좋다.

✅ 컴파일된 코드

// 추상 클래스 User1
class User1 {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
// 추상클래스 User1를 상속받은 Player1
class Player1 extends User1 {
    sayHello(name) {
        return `Hello ${name}!`;
    }
    fullName() {
        return `${this.firstName} + ${this.lastName}`;
    }
}

// ==========================================

// User2 인터페이스를 상속받은 Player2 
class Player2 {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    sayHello(name) {
        return `Hello ${name}!`;
    }
    fullName() {
        return `${this.firstName} + ${this.lastName}`;
    }
}

단점은?

인터페이스(interface)를 상속받은 클래스는
프로퍼티에 public이 강제된다는 단점이 있다.

TS커뮤니티에서는 클래스나 오브젝트를 정의할 때에는
인터페이스(interface)를 활용하고,
다른 경우에는 타입(type)을 활용하는 것을 추천한다고 한다.
또한 개발 환경에 따라 다르게 적용될 수도 있을 것 같다.


✅ 인터페이스(interface)와 제네릭(generic) 활용

interface Storage<T> {
    [key:string]: T
}

class LocalStorage<T> {
    private storage : Storage<T> = {}

    set(key: string, value: T){
        this.storage[key] = value;
    }
    remove(key: string){
        delete this.storage[key];
    }
    get(key: string): T {
        return this.storage[key];
    }
    clear(){
        this.storage = {};
    }

}

const stringStorage = new LocalStorage<string>();
const boolStorage = new LocalStorage<boolean>();

/*

1. 클래스 LocalStorage를 선언할 때, 
   제네릭(T)을 LocalStorage에서 사용한다고 선언 

2. storage 프로퍼티의 인터페이스(Storage)에도 사용한다고 선언

3. 인터페이스 Storage의 key,value 에서 value를 제네릭(T)으로 설정

*/

set함수를 사용하고자 할 때, 자동으로 value 타입을
string, boolean으로 설정해주는 것을 확인할 수 있다.

profile
wannabe---ing

0개의 댓글