나도 TypeScript로 일할 수 있다 01

kyc1996·2022년 7월 4일
0

JavaScript (TypeScript)

목록 보기
1/2

초과 프로퍼티 검사

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}

let mySquare = createSquare({ colour: "red", width: 100 });
}

createSquare 의 매개변수가 color 가 아니라 colour 로 잘못 전달된 것에 집중해보자.
이 경우 JavaSript에선 오류가 발생하지 않는다.

width 프로퍼티는 적합하고, color 프로퍼티는 없고, 추가 colour 프로퍼티는 중요하지 않기 때문이다.

하지만, TypeScript에서는 버그가 있을 수 있다고 생각한다.

객체 리터럴은 다른 변수에 할당할 때나 인수로 전달할 때, 특별한 처리를 받고 초과 프로퍼티 검사를 받는다.

만약 객체 리터럴이 대상 타입이 갖고있지 않은 프로퍼티를 갖고 있으면 에러가 발생하게 된다.

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

초과 프로퍼티 검사를 피하기 위해 사용하는 방법중에 하나다.

추가 프로퍼티가 있음을 확신한다면 [propName: string]: any; 와 같이 추가 프로퍼티에 대한 index signatuer를 추가해 준다.

하지만 위 방법 처럼 검사를 피하는 방법은 시도하지 않는것이 좋다.

초과 프로퍼티 에러의 대부분은 실제 버그이기 때문이다.

만약 옵션 백 같은 곳에서 초과 프로퍼티 검사 문제가 발생하면, 타입 정의를 조정해야 할 필요가 있다.

함수

선택적 매개변수와 기본 매개변수

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // 오류, 너무 적은 매개변수
let result2 = buildName("Bob", "Adams", "Sr.");  // 오류, 너무 많은 매개변수
let result3 = buildName("Bob", "Adams");         // 정확함

TypeScript 에서는 모든 매개변수가 함수에 필요하다고 가정한다.
이것은 null 이나 undefined를 줄 수 없다는 걸 의미하는 것이 아니라 컴파일러는 각 매개변수에 대해 사용자가 값을 제공했는지를 검사 한다는 것이다.
그렇기에 함수에 주어진 인자의 수는 함수가 기대하는 매개변수의 수와 일치해야 한다.

JavaScript 에서는 위의 설명과 다르게 매개변수에 값을 주지 않아도 된다.
그럴 경우 그 값은 undefined 가 된다.

TypeScript에서도 위와 같이 선택적 매개변수를 원한다면 매개변수 이름 끝에 ?를 붙임으로써 해결할 수 있다. 그 예시를 아래에서 보자.

function buildName(firstName: string, lastName?: string) {
  if (lastName)
    return firstName + " " + lastName;
  else
    return firstName;
}

let result1 = buildName("Bob"); // 바르게 동작한다. (lastName == undefined)
let result2 = buildName("Bob", "Adams", "Sr."); // 오류
let result3 = buildName("Bob", "Adams"); // 정확함

위처럼 제공하지 않은 매개변수 값에 대해서 undefined로 적용할 수도 있지만, 값을 제공하지 않아 undefined로 했을 때 할당 될 매개 변수의 값을 미리 지정해 놓을 수도 있다.

그 예시를 아래에서 보자.

function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // 올바르게 동작, "Bob Smith" 반환
let result2 = buildName("Bob", undefined);       // 여전히 동작, 역시 "Bob Smith" 반환
let result3 = buildName("Bob", "Adams", "Sr.");  // 오류, 너무 많은 매개변수
let result4 = buildName("Bob", "Adams");         // 정확함

나머지 매개변수

필수, 선택적, 기본 매개변수는 한번에 하나의 매개변수만을 가지고 이야기 한다.
때로는 다수의 매개변수를 그룹 지어 작업하기를 원하거나, 함수가 최종적으로 얼마나 많은 매개변수를 취할지 모를 때도 있을 것이다.

이 때, JavaScript에서는 모든 함수 내부에 위치한 arguments 라는 변수를 사용해 직접 인자를 가지고 작업한다.

TypeScript에서는 이 인자들을 하나의 변수로 모을 수 있다.

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

// employeeName 은 "Joseph Samuel Lucas MacKinzie" 가 될것입니다.
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

나머지 매개변수는 선택적 매개변수들의 수를 무한으로 취급한다. 나머지 매개변수로 인자들을 넘겨줄 때는 원하는 만큼 넘겨줄 수 있다. 또는 아무것도 넘기지 않을 수도 있다.

컴파일러는 생략 부호 (...) 뒤의 이름으로 전달된 인자 배열을 빌드하여 함수에서 사용할 수 있도록 한다.

생략 부호는 나머지 매개변수가 있는 함수의 타입에도 사용된다.

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfname.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

this 와 화살표 함수

JavaScript 에서, this는 함수가 호출될 때 정해지는 변수이다. 매우 강력하고 유연한 기능이지만 이것은 항상 함수가 실행되는 콘텍스트에 대해 알아야 한다는 수고가 생긴다.

특히 함수를 반환하거나 인자로 넘길 때의 혼란스러움은 악명 높다.

예시를 보자.

let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  card: Array(52),
  createCardPicker: function() {
    return function() {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);
      
      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
    }
  }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

createCardPicker가 자기 자신을 반환하는 함수임을 주목하라. 이 예제를 실행 시키면 오류가 발생할 것이다.

createCardPicker 에 의해 생성된 함수에서 사용 중인 thisdeck 객체가 아닌 window에 설정 되었기 때문이다.

최상위 레벨에서의 비-메서드 문법의 호출은 thiswindow로 한다.

이 문제는 나중에 사용할 함수를 반환하기 전에 바인딩을 알맞게 하는 것으로 해결할 수 있다.

이 방법대로라면 나중에 사용하는 방법에 상관없이 원본 deck 객체를 계속해서 볼 수 있다. 이를 위해 함수의 표현식을 ES6의 화살표 함수로 바꿀 것이다.

화살표 함수는 함수가 호출 된 곳이 아닌 함수가 생성된 쪽의 this를 캡처한다.

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // NOTE: 아랫줄은 화살표 함수로써, 'this'를 이곳에서 캡처할 수 있도록 한다
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

this 매개변수

화살표 함수를 활용해 this에 발생하는 에러를 없앴지만 this.suits[pickedSuit] 의 타입은 여전히 any 이다.

this가 객체 리터럴 내부의 함수에서 왔기 때문인데, 이것을 고치기 위해 명시적으로 this 매개변수를 줄 수 있다. this 매개변수는 함수의 매개변수 목록에서 가장 먼저 나오는 가짜 매개변수이다.

명확하고 재사용하기 쉽게 CardDeck 두 가지 인터페이스 타입들을 예시에 추가해 보자.

interface Card {
  suit: string;
  card: number;
}

interface Deck {
  suits: string[];
  cards: number[];
  createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  
  createCardPicker: function(this: Deck) {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);
      
      return {suit: this.suits[pickedSuit], card: pickedCard % 13};
    }
  }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);
  

이제 TypeScript는 createCardPickerDeck 객체에서 호출된다는 것을 알게 됐다.
이것은 thisany 타입이 아니라 Deck 타입이며 따라서 --noImplicitThis 플래그가 어떤 오류도 일으키지 않는다는 것을 의미한다.

리터럴 타입

리터럴 타입 좁히기

var 또는 let으로 변수를 선언할 경우 이 변수의 값이 변경될 가능성이 있음을 컴파일러에게 알린다. 반면, const로 변수를 선언하게 되면 TypeScript에게 이 객체는 절대 변경되지 않음을 알린다.

무한한 수의 잠재적 케이스를 유한한 수의 잠재적 케이스로 줄여나가는 것을 타입 좁히기라 한다.

문자열 리터럴 타입

실제로 문자열 리터럴 타입은 유니언 타입, 타입 가드 그리고 타입 별칭과 잘 결합된다. 이런 기능을 함께 사용하여 문자열로 enum과 비슷한 형태를 갖출 수 있다.

type Easing = "ease-in" | "ease-out" | "ease-in-out";

class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === "ease-in"){
     // ... 
    } else if (easing === "ease-out") {
    } else if (easing === "ease-in-out") {
    } else {
      // type을 무시하면 여기로.
    }
  }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // Easing에 포함되지 않음 -- error

허용된 세 개의 문자열이 아닌 다른 문자열을 사용하게 되면 오류가 발생한다.

문자열 리터럴 타입은 오버로드를 구별하는 것과 동일한 방법으로 사용될 수 있다.

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... 추가적인 중복 정의들 ... 
function createElement(tagName: string): Element {
  // ... 여기에 로직 추가 ...
}

숫자형 리터럴 타입

TypeScript에는 위의 문자열 리터럴과 같은 역할을 하는 숫자형 리터럴 타입도 있다.

function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6;
}

const result = rollDice();

이는 주로 설정값을 설명할 때 사용된다.

// loc/lat 좌표에 지도를 생성한다.
declare function setupMap(config: MapConfig): void;
// ---생략---
Interface MapConfig {
  lng: number;
  lat: number;
  tileSize: 8 | 16 | 32;
}

setupMap({ lng: -73.935242, lat: 40.73061, tileSize: 16 });
profile
4학년 취준생

0개의 댓글