개발 공부/타입스크립트

타입스크립트 핸드북 (1~4)

5묘 2023. 3. 27. 18:42

https://joshua1988.github.io/ts/guide/functions.html#this

 

함수 | 타입스크립트 핸드북

타입스크립트에서의 함수 웹 애플리케이션을 구현할 때 자주 사용되는 함수는 타입스크립트로 크게 다음 3가지 타입을 정의할 수 있습니다. 함수의 파라미터(매개변수) 타입 함수의 반환 타입

joshua1988.github.io

1. 기본타입

기본 타입은 크게 12가지가 있다.

Boolean - 타입이 진위값인 경우
let isLoggedIn : boolean = false;

Number - 타입이 숫자일 때
let num : number = 10;

String - 타입이 문자일 때
let str : string = 'hi';

Object 

Array - 타입이 배열인 경우는 아래처럼 선언
let arr: Array<number> = [1, 2, 3]

Tuple - 배열의 길이가 고정되고, 각 요소의 타입 지정되어있는 배열 형식
let arr: [string, number] = ['hi', 10];

Enum - 자바, C에서 흔히 쓰이는 타입으로 특정값(상수)들의 집합을 의미
enum Avengers {Capt, IronMan, Thor}
let hero : Avengers = Avengers.Capt;

Any - 모든 타입에 대해 허용

Void - 변수에 undefined와 null만 할당하고, 함수에는 반환값을 설정할 수 없는 타입
let unuseful : void = undefined;
function notuse() : void{
   console.log('sth');
}

Null
Undefined

Never - 함수의 끝에 절대 도달하지 않는다는 의미를 지닌 타입
// 이 함수는 절대 함수의 끝까지 실행되지 않는다는 의미
function neverEnd() : never {
	while (true) {
    }
}

2. 함수

웹 애플리케이션 구현 시 자주 사용되는 함수는 타입스크립트로 크게 다음 3가지 타입 정의할 수 있다.

1. 함수의 파라미터(매개변수) 타입
2. 함수의 반환 타입
3. 함수의 구조 타입

# 함수의 기본적인 타입 선언: 
	매개변수와 합수의 반환값에 타입을 추가 (반환값 타입 정하지 않을 때는 void라도 사용)
    function sum(a: number, b: number) : number {
        retutn a + b;
    }

# 함수의 인자:
	타입스크립트에서는 함수의 인자를 모두 필수값으로 간주한다. 
    따라서, 함수의 매개변수 설정 시, undefined나 null 이라도 인자로 넘겨야 하며, 
    컴파일러에서 정의된 매개변수 값이 넘어왔는지 확인한다. 정의된 매개변수 값 외에 추가로 값을 받을 순 없다.
    
    하지만 자바스크립트에서는 매개변수 갯수만큼 인자를 넘기지 않아도 된다.
    이 특성 살리고 싶으면 ? 을 이용하다.
    
    매개변수 초기화 시에는 타입을 붙이지 않는다.
    function sum(a: number, b='100'): number {
        return a + b; 
    }

# ES6 문법에서 지원하는 REST 문법
	function sum(a: number, ...nums: number[]) : number {
    	const totalOfNums = 0;
        for (let key in nums) {
        	totalOfNums += nums[key];
        }
        return a + totalOfNums;
    }

 

cf) javascript rest 문법(https://learnjs.vlpt.us/useful/07-spread-and-rest.html)

const purpleCuteSlime = {
  name: '슬라임',
  attribute: 'cute',
  color: 'purple'
};

const { color, ...rest } = purpleCuteSlime;
console.log(color);
console.log(rest);

//꼭 rest라 쓰지 않아도 됨.

const purpleCuteSlime = {
  name: '슬라임',
  attribute: 'cute',
  color: 'purple'
};

const { color, ...cuteSlime } = purpleCuteSlime;
console.log(color);
console.log(cuteSlime);

const { attribute, ...slime } = cuteSlime;
console.log(attribute);
console.log(slime);
// 함수에서 rest를 사용할 때
// 다음의 경우 파라미터가 7개라 인자 7개가 가야 하는데 6개만 오기 때문에 마지막 하나인 g가 undefined가 돼서
// 합했을 때 숫자 + undefined = NaN이 되어버린다.

function sum(a, b, c, d, e, f, g) {
  let sum = 0;
  if (a) sum += a;
  if (b) sum += b;
  if (c) sum += c;
  if (d) sum += d;
  if (e) sum += e;
  if (f) sum += f;
  if (g) sum += g;
  return sum;
}

const result = sum(1, 2, 3, 4, 5, 6);
console.log(result);

// 이를 방지하기 위해 아래와 같이 rest를 사용할 수 있다.
function sum(...rest) {
  return rest.reduce((acc, current) => acc + current, 0);
}

const result = sum(1, 2, 3, 4, 5, 6);
console.log(result); // 21

this가 가리키는 것 명시하려면 아래와 같은 문법 써준다.

interface Vue {
    el: string;
    count: number;
    init(this: Vue) : () => {};
}

let vm: Vue = {
    el: '#app',
    count: 10,
    init: function(this: Vue) {
        return () => {
            return this.count;
        }
    }
}

let getCount = vm.init();
let count = getCount();
console.log(count);

만약 콜백으로 함수가 전달되었을 때 this를 구분해줘야 하는데 이럴 땐 아래와 같이 강제할 수 있다.

interface UIElement {
    //아래의 함수에서 'this:void' 코드는 함수에 'this' 타입을 선언할 필요가 없다는 의미입니다.
    addClickListener(onclick: (this: void, e: Event) => void) : void;
}

// class Handler {
//     info: string;
//     onClick(this: Handler, e: Event) {
//         // 위의 `UIElement` 인터페이스의 스펙에 this는 필요없다 했지만 사용했기 때문에 에러가 발생합니다.
//         this.info = e.message;
//     }
// }

class Handler {
    info: string;
    onClick(this: void, e: Event) {
        console.log('clicked')
    }
}


let handler = new Handler();
uiElement.addClickListener(handler.onClick);

이게 바로 클래스의 메서드 방식으로 선언하는 것과 변수에 화살표 함수를 연결하는 것의 차이점이다.

3. 인터페이스

인터페이스는 상호 간에 정의한 약속, 혹은 규칙을 의미한다.
타입스크립트에서 인터페이스는 보통 다음의 범주에 대해 약속을 정의한다.

- 객체의 스펙(속성과 속성의 타입)
- 함수의 파라미터
- 함수의 스펙(파라미터, 반환 타입 등)
- 배열과 객체를 접근하는 방식
- 클래스

# 인터체이스에 대해 알아볼 간단한 예제를 보자

    let person = {naem: 'Capt', age: 28};
    function logAge(obj: {age: number}) { // 인자를 받을 때 객체의 속성타입까지 정의 가능
        console.log(obj.age);
    }
    logAge(person); //28

    만약 여기에 인터페이스를 적용한다면? 
    interface personAge {
        age: number;
    }

    function logAge(obj: personAge) { //인자가 'personAge'란 이름 얻어 보다 명시적으로 바뀜
        console.log(obj.age);
    }

    let person = {name: 'Capt', age: 28};
    logAge(person);

    인터페이스를 인자로 받아 사용할 때는 '인터페이스에 정의된 속성 개수'가 '인자의 속성 개수' 보다 적어도 된다. 
    하지만 인자의 속성은 인터페이스에 정의된 속성 개수는 반드시 포함해야 한다.(옵션 속성 제외)
    또 속성 순서를 지키지 않아도 된다.

# 옵션 속성: 속성에 ? 붙이기.
    interface 인터페이스_이름 {
      속성? : 타입;
    }

# 옵션 속성의 장점
    단순히 인터페이스 사용할 때 속성 선택적으로 적용가능한 것 뿐 아님!
    인터페이스에 정의되지 않은 속성을 인지시키는 것이 가능하다는 것이다.
    
 # 읽기 전용 속성
     읽기 전용 속성은 인터페이스로 객체 처음 생성할 때만 값 할당하고, 그 이후에는 변경 불가능한 속성을 의미한다.
     문법은 다음과 같이 readOnly 속성을 앞에 붙인다.
    interface CraftBeer {
       readonly brand: string;
    }
    
# 읽기 전용 배열
    배열 선언 시 ReadonlyArray<T> 타입 사용하면 읽기 전용 배열 생성 가능하다.
    
    let arr: ReadonlyArray<number> = [1, 2, 3];
    arr.splice(0,1); //errror
    arr.push(4);  //errror
    arr[0] = 100; //errror
    
    이처럼 배열을 ReadonlyArray로 선언 시 배열의 내용 변경할 수 없다. 선언 시점에만 값 정의 가능하다.

# 객체 선언과 관련된 타입 체킹
    타입스크립트는 인터페이스를 이용해 객체를 선언할 때 좀 더 엄밀한 속성 검사를 진행한다.
    만약 까다로운 타입 추론을 무시하고 싶다면 아래와 같이 선언한다.

    let myBeer = { brandon: 'what' };
    brewBeer(myBeer as CraftBeer);

    그럼에도 불구하고 인터페이스에서 정의하지 않은 속성들을 추가로 사용하고 싶다면?
    이렇게 하면 된다.

    interface CraftBeer {
      brand?: string;
      [propName: string]: any;
    }

# 함수 타입
    인터페이스는 함수의 타입을 정의할 때도 쓸 수 있다.

    interface login {
        (username: string, password: string): boolean;
    }

    let loginUser : login;
    loginUser = function(id: string, pw: string) {
        console.log('로그인 했습니다.');
        return true;
    }
    
# 클래스 타입
    C#이나 자바처럼 타입스크립트에서도 클래스가 일정 조건을 만족하도록 타입 규칙을 정할 수 있다.
    interface CraftBeer {
        beerName: string;
        nameBeer(beer: string) : void;
    }

    class myBeer implements CraftBeer {
        beerName: string = 'Baby Guinness';
        nameBeer(b: string) {
            this.beerName = b;
        }
        constrouctor () {}
    }

    const newBeer = new myBeer();
    console.log(newBeer.beerName);
    console.log(newBeer.nameBeer('Good Shot'));
    console.log(newBeer.beerName);

# 인터페이스 확장
    클래스와 마찬가지로 인터페이스도 인터페이스 간 확장이 가능하다.
    interface Person {
        name: string;
    }

    interface Developer extends Person {
        skill : string;
    }

    let fe = {} as Developer
    fe.name = 'josh';
    fe.skill = 'TypeScript';

    혹은 아래와 같이 여러 인터페이스를 상속받아 사용할 수 있다.
    interface Person {
        name: string;
    }

    interface Drinker {
        drink: string;
    }

    interface Developer extends Person, Drinker {
        skill: string;
    }

    let fe = {} as Developer;
    fe.name = 'josh';
    fe.skill = 'TypeScript';
    fe.drink ='Beer';

# 하이브리드 타입
    인터페이스는 다음과 같이 함수 타입이면서 객체 타입을 정의할 수 있는 인터페이스처럼, 
    여러가지 타입을 조합해 만들 수 있다.

    interface CraftBeer {
        (beer: string) : string;
        brand: string;
        brew() : void;
    }

    function myBeer() : CraftBeer {
        let my = (function(beer:string) {}) as CraftBeer;
        my.brand = 'Beer Kitchen';
        my.brew =  function() {};
        return my
    }

    let brewedBeer = myBeer();
    brewedBeer('My First Beer');
    brewedBeer.brand = 'Pangyo Craft';
    brewedBeer.brew();
    
# 클래스를 상속받는 인터페이스
interface가 클래스의 상속을 받을 때 내부의 private, protected 속성들도 모두 상속받는다.
이렇게 클래스를 상속받는 interface를 implement하려면 클래스나 하위 클래스 역시 extends(상속) 받아야 한다.

class Control {
  private state: any;
}
 
interface SelectableControl extends Control {
  select(): void;
}
 
class Button extends Control implements SelectableControl {
  select() {}
}
 
class TextBox extends Control {
  select() {}
}
 
class ImageControl implements SelectableControl {
// Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
  private state: any;
  select() {}
}

4. 이넘

이넘은 특정 값들의 집합을 의미하는 자료형.

1. 숫자형 이넘 선언
enum Direction {
Up = 1, //1
Down,  //2
Left,  //3
Right //4
}

숫자형 이넘에 초기값을 주면 초기값부터 차례로 1씩 증가
초기값 주지 않으면 0부터 차례로 1씩 증가

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

숫자형 이넘은 다음처럼 사용 가능하다.

enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Captain Pangyo", Response.Yes);

문자형 이넘은 반드시 초기값을 줘야 한다
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

복합 이넘(권고X, 최대한 같은 타입으로 이루어진 이넘 사용 추천)
enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

런타임 시점에서의 이넘 => 런타임 시 이넘은 실제 객체 형태로 존재한다.
가령 아래와 같은 이넘 코드 있을 때

enum E {
  X, Y, Z
}

function getX(obj: { X: number }) {
  return obj.X;
}
getX(E); // 이넘 E의 X는 숫자이기 때문에 정상 동작

컴파일 시점에서의 이넘 => 런타임에서는 실제객체이지만 keyof 대신 keyof typeof를 사용해야 한다.
enum LogLevel {
    ERROR, WARN, INFO, DEBUG
}

//'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
type LogLevelStrings = keyof typeof LogLevel;

function printImportant(key:LogLevelStrings, message: string) {
    const num = LogLevel[key];
    if (num <= LogLevel.WARN) {
        console.log('Log Level key is',key);
        console.log('Log Level value is',num);
        console.log('Log Level message is',message)
    }
}
    printImportant('ERROR', 'This is Message')
    
    리버스 매핑 => 숫자형 이넘에만 존재하는 특징. 이넘의 키(key) 로 값을 얻을 수 있고 값으로 키를 얻을 수 있다.
    
enum Enum {
	A
  }

let a = Enum.A; // 키로 값을 획득
let keyName = Enum[a]; // 값으로 키를 획득