타입스크립트 핸드북

nn·2022년 6월 11일
0

타입스크립트 핸드북을 요약 정리한 내용입니다.

타입스크립트 기본 타입

String, Number, Boolean

let str : string = 'hi'
let num : number = 10;
let isLoggedIn : boolean = false;

위와 같이 :를 이용하여 자바스크립트 코드에 타입을 정의하는 방식을 타입 표기(Type Annotation)라고 한다.


Object

Array

  • 배열
let arr : number[] = [1,2,3];
  • 제네릭 사용
let arr = Array<number> = [1,2,3];

Tuple

튜플은 길이와 요소가 고정되어있는 배열이다.

let arr : [string, number] = ['hi', 10]

Enum

특정 상수 값들의 집합

enum fruits {apple, banana, orange}
let food : fruits = fruites.apple;
  • 인덱스 번호로 접근하기
enum fruits {apple, banana, orange}
let food : fruits = fruites[0];
  • 인덱스 변경하기
// apple의 인덱스번호를 2로 지정
// banana,orange는 3,4가 된다.
enum fruits {apple=2, banana, orange}
let food : fruits = fruits[2]; // apple
let food : fruits = fruits[4]; // orange 

Any

타입이 지정되어있지 않은 자바스크립트 코드를 타입스크립트에 적용할 때 사용하기 좋은 타입이다.
즉, 모든 타입을 허용한다.

let str: any = 'hi';
let num: any = 10;
let arr: any = ['a', 2, true];

Void

반환 값이 없는 함수를 설정할 때 사용하는 타입이다.
변수를 선언할때는 undefinednull만 할당가능하다.

let unuseful: void = undefined;
function notuse(): void {
  console.log('sth');
}

Never

함수가 절대 끝나지 않는 의미를 가질때 사용한다.

// 이 함수는 절대 함수의 끝까지 실행되지 않는다는 의미
function neverEnd(): never {
  while (true) {

  }
}

함수

타입스크립트에서 함수를 선언할 때 3가지의 타입을 정의할 수 있다.

  • 함수의 파라미터(매개변수) 타입
  • 함수의 반환 타입
  • 함수의 구조 타입

함수의 인자

타입스크립트에서는 함수의 인자를 모두 선언해 주어야한다.
그러므로 매개변수를 설정했다면 매개변수의 값이 제대로 넘어왔는지 확인을 해주어야 한다.

만약 함수가 가지는 인자의 갯수가 때에 따라 달라진다면 ?키워드를 사용해 정의 할 수 있다.

function sum(a: number, b?: number): number {
  return a + b;
}
sum(10, 20); // 30
sum(10, 20, 30); // error, too many parameters
sum(10); // 10

sum은 매개변수로 ab 혹은 a하나만으로도 가질 수 있다.

다음과 같이 매개변수를 정의와 동시에 초기화할 수 있다.

function sum(a: number, b = '100'): number {
  return a + b;
}
sum(10, undefined); // 110
sum(10, 20, 30); // error, too many parameters
sum(10); // 110

REST문법이 적용된 매개변수

  • rest매개변수는 함수의 매개변수가 몇 개가 될 지 모르는 상황에서 유용하다.
function sum(a: number, ...nums: number[]): number {
  let totalOfNums = 0;
  for (let key in nums) {
    totalOfNums += nums[key];
  }
  return a + totalOfNums;
}
console.log(sum(1,2,3,4,5)); // 15
console.log(sum(1,2,3)); // 6

에러정리

const arr1 : number[] = [1,2,3,4,5];
const arr2 : number[] = [1,2,3];

console.log(sum(arr1));
console.log(sum(arr2));

...nums : number[]를 매개변수로 받기 때문에 number[]으로 선언된 변수를 sum함수의 매개변수로 보내려고 했지만 컴파일에러가 발생했다.

...nums : number[]는 nums를 분해했을 때 number[]타입을 가져야하는 변수이다.
즉, ...nums: number[][[1,2,3]] 의 형태를 가져야만 nums를 펼쳤을때 number[]타입이 나오게 되는 것이다.

--> nums : [number[]]
--> ...nums : number[]

그러므로 매개변수에 선언된 a:number에 의해 매개변수 number타입인 1,2,3 등이 올수 있으며 이들은 ...nums : number[]에 의해 [1,2,3] 배열의 형태로 받게 된다.

this

타입스크립트에서 this를 사용하기 위해선 다음과 같은 문법을 사용한다 .

function 함수명(this: 타입) {
  // ...
}

this를 사용해 인터페이스에 선언된 변수의 getter역할을 할 수 있습니다.

interface Iam {
  name: string;
  age: number;
  getAge(this: Iam): () => {};
}

let iam: Iam = {
  name: 'seungha',
  age: 26,
  getAge: function(this: Iam) {
    return () => {
      return this.age;
    }
  }
}

let myAge = iam.getAge();
let age = myAge();
console.log(age); // 26

콜백에서의 this

콜백으로 함수가 전달되었을때도 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;
    }
}
let handler = new Handler();
uiElement.addClickListener(handler.onClick); // error!

인터페이스

function logAge(obj: { age: number }) {
  console.log(obj.age); // 28
}

logAge() 함수는 number타입을 속성으로 가지는 객체를 인자로 받는다.

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

인터페이스를 적용하면 logAge()의 인자를 좀 더 명시적으로 작성할 수 있다.

interface personAge {
  age: number;
}
function logAge(obj: personAge) {
  console.log(obj.age);
}
let person = { name: 'Capt', age: 28 };
logAge(person); 

logAge()함수는 personAge라는 타입을 인자로 가져야하고 personAge인터페이스는 number타입의 age속성을 가지고 있다.

인터페이스를 인자로 받아 사용하면 인자로 받은 객체의 속성갯수(person 속성 두 개) 와 인터페이스의 속성(personAge 속성 한 개) 의 개수가 일치하지 않아도 된다.
즉, 객체가 가진 속성 중 인터페이스에 정의된 속성, 타입을 만족하는 것이 있기만 하면 되므로 속성의 갯수와 순서를 신경쓰지 않아도 된다.

옵션 속성

인터페이스를 사용 할 때 인터페이스에 정의된 속성을 모두 사용하지 않아도 된다.

이를 옵션 속성이라고 한다.
옵션 속성은 ?를 붙여사용한다.

interface CraftBeer {
  name: string;
  hop?: number;  
}
let myBeer = {
  name: 'Saporo'
};

인터페이스의 속성이 두개 임에도 불구하고?를 붙인 hop속성을 정의하지 않아도 선언이 가능하다.

이처럼 옵션속성은 인터페이스를 사용할 때 속성을 선택적을 적용할 수 있기도 하지만, 인터페이스에 정의되지 않은 속성에 대해선 오류를 표시해주어 속성을 인지 시켜주기도한다.

읽기 전용 속성

인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경하지 않는 속성에는 readOnly를 붙여준다.

interface CraftBeer {
  readonly brand: string;
}

readOnly를 수정하려고 하면 에러가 발생한다.

let myBeer: CraftBeer = {
  brand: 'Belgian Monk'
};
myBeer.brand = 'Korean Carpenter'; // error!

읽기 전용 배열

배열을 선언할 때 ReadonlyArray<T> 타입을 사용하면 읽기 전용 배열을 생성할 수 있다.

let arr: ReadonlyArray<number> = [1,2,3];
arr.splice(0,1); // error
arr.push(4); // error
arr[0] = 100; // error

클래스 타입

클래스가 일정 조건을 만족하도록 타입 규칙을 지정할 수 있다.

interface CraftBeer {
  beerName: string;
  nameBeer(beer: string): void;
}

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

인터페이스 확장

interface Person {
    name : string
}

interface Developer extends Person {
    skill : string 
}

let dev = {} as Developer;
dev.name = 'seungha';
dev.skill = 'ts';

하이브리드 타입

자바스크립트의 유연한 특성을 따라 인터페이스도 여러 타입을 조합하여 만들수 있다.
다음은 함수 타입이면서 객체 타입을 정의하는 인터페이스이다.

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

function myBeer() : CraftBeer {
    let my = (function(beer:string) {console.log (`1 beer name: ${beer}`)} ) as CraftBeer;
    my.brand = 'beer kitchen';
    my.brew = function() { console.log ("2 brew()", `3 brand ${this.brand}`)}
    return my;
}

let brewedBeer = myBeer();
brewedBeer("function type beer");
brewedBeer.brand = "beer brand";
brewedBeer.brew();

이넘

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

숫자형 이넘

enum Message {
  // auto-incrementing
  Yes, // or Yes = 0
  No // or  No = 1
}

function respond(name : string , message : Message){
    console.log(name, message);
}

respond("seungha", Message.Yes) 
//"seungha", 0

문자형 이넘

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

문자형 이넘은 숫자형 이넘과 달리 이넘 값 모두를 특정 문자 혹은 다른 이넘 값으로 초기화를 해줘야한다.

또한, 숫자형 이넘과 같은 auto-incrementing이 없으므로 주의해야한다.

복합 이넘

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

문자와 숫자를 혼합하여 이넘을 생성할 수 있으나 권고하는 방식은 아니다.
최대한 같은 타입으로 이루어진 이넘을 생성하자.

컴파일시 이넘

이넘은 런타임시에는 객체이다
그러므로 이넘 타입을 typeof로 사용하면 이넘 타입이 아닌 객체 자체가 타입이 되어버린다.
즉, 인터페이스와 비슷하게 사용할 수 밖에 없어진다.

그럼 이넘 타입을 사용하기 위해선 어떻게 해야할까?
keyof를 사용해 이넘 객체의 프로퍼티들을 뽑아내자.

enum LogLevel {
  ERROR, WARN, INFO, DEBUG
}

// 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
// LogLevelStrings 이넘 타입 사용
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 a message');

keyof typeof를 사용하여 이넘 객체 자체를 구하고, 객체의 키를 추출하여 사용하자.

연산자를 이용한 타입 정의

Union Type (or || 연산자)

function getAge(age: string | number) {
  // ...
}

위 함수는 인자로 문자열 타입이나 숫자 타입이 모두 가능한 함수이다.
|를 사용하여 타입을 여러개 연결하는 것을 유니온 타입 정의 방식이라고 한다 .

any를 사용하여 모든 타입을 허용할 수 있지만, 이런경우 타입스크립트가 타입을 추론할 수 없으므로 유니온 타입을 잘 활용하자.

Intersection Type

여러타입을 모두 만족하는 하나의 타입을 인터섹션 타입이라고 한다.

interface Person {
  name: string;
  age: number;
}
interface Developer {
  name: string;
  skill: number;
}
type Capt = Person & Developer;

&를 사용하여 두 인터페이스의 정의를 합쳤다.
결과적으로 Capt의 타입은 다음과 같이 정의된다.

{
  name: string;
  age: number;
  skill: string;
}

Union Type을 쓸 때 주의할 점

interface Person {
  name: string;
  age: number;
}
interface Developer {
  name: string;
  skill: string;
}
function introduce(someone: Person | Developer) {
  someone.name; // O 정상 동작
  someone.age; // X 타입 오류
  someone.skill; // X 타입 오류
}

파라미터의 타입이 Person도 되고 Developer도 될테니까 ageskill를 사용할 수 있겠지라고 생각할 수 있으나,
공통적으로 들어있는 속성인 name에만 접근 할 수 있다.


클래스

readonly

readOnly를 클래스의 속성에 적용하면 접근만 가능하게 된다.

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

따라서 readonly를 사용하면 생성자 함수에 초기 값 설정을 지정하면서 인자에 readonly를 함께 추가하여 사용하면 유용하다.

Accessor

객체의 특정 속성의 접근과 할당을 제어하기 위해선 해당 객체를 클래스로 생성해야한다.

class Developer {
  private name: string;
  
  get name(): string {
    return this.name;
  }

  // name속성에 제약사항 추가
  set name(newValue: string) {
    if (newValue && newValue.length > 5) {
      throw new Error('이름이 너무 깁니다');
    }
    this.name = newValue;
  }
}
const josh = new Developer();
josh.name = 'Josh Bolton'; // Error
josh.name = 'Josh';

getset을 사용하여 할당되는 값을 제어 할 수 있다.

get만 선언하고 set을 선언하지 않는 경우 자동으로 readonly로 인식된다.

Abstract Class

추상 클래스는 인터페이스와 비슷하지만 특정한 클래스의 상속 대상이 되는 역할로, 상위레벨에서 속성등을 정의할 때 사용한다.

abstract class Developer {
  abstract coding(): void; // 'abstract'가 붙으면 상속 받은 클래스에서 무조건 구현해야 함
  drink(): void {
    console.log('drink sth');
  }
}

class FrontEndDeveloper extends Developer {
  coding(): void {
    // Developer 클래스를 상속 받은 클래스에서 무조건 정의해야 하는 메서드
    console.log('develop web');
  }
  design(): void {
    console.log('design web');
  }
}
const dev = new Developer(); // error: cannot create an instance of an abstract class
const josh = new FrontEndDeveloper();
josh.coding(); // develop web
josh.drink(); // drink sth
josh.design(); // design web

제네릭

제네릭은 타입을 함수의 인자처럼 사용하는 것을 말한다.

function getText(text) {
  return text;
}
getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true

위 함수는 text를 인자로 받아 그대로 반환해준다.
text는 어떤한 타입이 들어가도 반환이 된다.

이를 제네릭 문법을 적용하면 다음과 같은 형태가 된다.
이처럼 제네릭을 사용하면 any 타입을 사용했을때 타입검사를 하지 않아 어떤 값이 반환되는지 알 수 없게되는 문제를 피할 수 있으며, 가독성도 좋아진다.

function getText<T>(text: T): T {
  return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);

예를들어 getText<string>('hi');는 다음과 같이 작동하게 된다.

function getText<string>(text: string): string {
  return text;
}

제네릭 타입 변수

function logText<T>(text: T): T {
  console.log(text.length); // Error: T doesn't have .length
  return text;
}

위 코드는 text의 length를 확인하기 알기위해서 작성한 코드이다.
하지만 컴파일 에러가 발생한다.

위 코드를 다시 보자.
<T>any처럼 작동한다.
즉, 문자열이나 배열 숫자가 들어올 수는 있지만 숫자인 number가 들어왔을 때에는 .length가 유효하지 않기때문이다.

이런 경우에는 제네릭에 타입을 주면 된다.

function logText<T> (text : Array<T>) : Array<T> {
    console.log(text.length);
    return text;
}

제네릭타입으로 인자를 받고 인자 값을 배열형태로 받는다.
가령, [1,2,3]이라는 숫자 배열을 받게된다면 반환값은 number가 될 것이다.

제네릭 타입

function logText<T> (text : T) : T {
    return text;
}
// 1
let str : <T>(text : T) => T = logText;
// 2
let str : {<T>(text: T) : T} = logText;

logText 함수를 사용하는 두 코드는 같은 의미를 가진다.
logText함수는 <T> 제네릭이며 인자로 가지는 textT 제네릭이고 리턴타입 또한 T이다.

이 방식으로 인터페이스를 작성할 수 있다.

interface GenericLogTextFn {
    <T>(text: T): T;
}

function logText<T> (text : T) : T {
    return text;
}

let my : GenericLogTextFn = logText;

GenericLogTextFn 인터페이스가 없었다면
let my : {<T>(text : T) : T} = logText; 처럼 타입을 명시하면서 적어야 했을 것이다.
GenericLogTextFn 를 적용하여 가독성을 높일 수 있다.

제네릭 클래스

제네릭은 인터페이스 뿐만 아니라 클래스도 생성할 수 있다.
이넘과 네임스페이스는 제네릭으로 생성할 수 없다.

class GenericMath<T> {
  pi: T;
  sum: (x: T, y: T) => T;
}

let math = new GenericMath<number>();

클래스 명에 <T>를 함께 적어주고, 클래스로 인스턴스를 생성할 때 타입을 함께 지정해주면 된다.

제네릭 제약 조건

제네릭으로 선언한 인자에 어떤 타입이 들어왔는지 확인 할 수 있는 방법을 알아보자.

function logText<T>(text: T): T {
  console.log(text.length); // Error: T doesn't have .length
  return text;
}

앞서 말했듯이 T에는 어떤 타입이 들어올지 모르므로 .length에서 오류가 난다.

동작하는 인자만 받을 수 있도록 아래와 같이 작성할 수 있다.

interface LengthWise {
    length : number ;
}

function logText<T extends LengthWise>(text: T) :T {
    console.log(text.length);
    return text;
}
logText({ length: 0, value: 'hi' }); 

// `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음

length에 대해 동작하는 인자만 받을 수 있도록 인터페이스를 추가 해주 었다.

객체의 속성을 제약하는 방법

두개의 객체를 비교할 때도 제네릭을 사용할 수 있다.

function getProperty<T, O extends keyof T> (obj : T, key : O) {
    return obj[key];
}
let o = {a:1,b:2,c:3};

getProperty(o, "a");
getProperty(o, "x"); //error;

위 코드에서는keyof를 사용하여 두번째 인자에는 첫번째 인자의 키에 해당하는 객체만이 올 수 있도록 제한하였다.

타입 추론

Best Common Type

타입스크립트는 Best Common Type 알고리즘에 의해 타입을 추론하게 된다.

let arr = [0, 1, null]

arrnumbernull로 이루어진 배열이다.
이 때 Best Common Type 알고리즘으로 다른 타입들과 가장 잘 호환되는 타입을 선정하여
arr 의 타입을 let arr: (number | null)[]로 추론한다.

문맥상의 타이핑

타입스크립트는 문맥(코드 위치)으로도 타입을 결정한다.

window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.button);   //<- OK
  console.log(mouseEvent.kangaroo); //<- Error!
};

타입스크립트는 window.onmousedown에 할당되는 함수의 타입을 추론하기 위해 window.onmousedown 타입을 먼저 검사한다.

window.onmousedown 타입은 마우스의 이벤트와 관련이 있다고 알아내면 마우스에 button은 있지만 kangaroo 속성은 없다고 판단한다.

타입스크립트의 타입 체킹

타입스크립트가 타입체킹을 할 때는 값의 형태에 기반하여 이루어진다.
이것을 Duck Typing 또는 Structural Subtyping 이라고 한다.

  • Duck Typing
    객체의 변수 및 메서드의 집합이 객체의 타입을 결정하는 것을 의미. 동적 타이핑의 한 종류
  • Structural Subtyping
    객체의 실제 구조나 정의에 따라 타입을 결정하는 것을 의미

타입호환

타입호환이란 타입스크립트에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다.

interface Ironman {
  name: string;
}

class Avengers {
  name: string;
}

let i: Ironman;
i = new Avengers();

자바스크립트는 객체 리터럴이나 익명함수를 사용하여 작동하기 때문에, 명시적으로 타입을 지정하는 것보다는 코드의 구조관점으로 타입을 지정한다.

이것이 위 코드에서 IronmanAvengers는 상속관계이 있지 않지만 에러가나지 않는 이유이다.

구조적 타이핑

구조적 타이핑이란 코드 구조 관점에서 타입이 서로 호환되는지의 여부를 판단하는 것이다.

interface Avengers {
    name : string;
}

let hero : Avengers ;
let capt = {name : "seunghaahha", location : "seoul"}
hero = capt;

Avengers의 속성에 name이 있으므로 herocapt이 대입이 가능하다.

Soundness

타입스크립트는 컴파일 시점에 타입을 추론할 수 없어도 일단 안전하다고 보는 특성이 있다.

profile
내가 될 거라고 했잖아

0개의 댓글