28_타입스크립트 보다 자세한 문법
FrontEnd/JavaScript

28_타입스크립트 보다 자세한 문법

728x90

이전에 타입스크립트를 간단히 배우고 리액트에 적용을 해보았는데, 타입스크립트의 문법을 조금 더 자세하게 알아보자.

 

타입스크립트는 강력한 타입으로 대규모 애플리케이션 개발에 용이하며 유명한 자바스크립트 라이브러리와의 편리한 사용이 가능하다. 또한 개발 도구에서 강력한 지원이 가능하다는 장점이 있다.

 

이전에 말했듯이 타입스크립트를 이용하면 많은 실수를 줄일 수 있다라는 장점이 있다!

 

yarn add typescript -g

다음과 같이 타입스크립트를 전역적으로 설치해주자.

 

//01_hello.ts
var hello = "hello";
let hello2 = "hello2";

 

다음과같은 간단한 예제코드를 작성하고

 

tsc .\01_hello.ts

 

터미널에서 tsc파일로 바꾸어주면 01_hello.js파일이 생성된 것을 알 수 있다.

 

//01_hello.js

var hello = "hello";
var hello2 = "hello2";

typescript는 구형 브라우저도 지원하기 때문에 var로 바뀌어서 나오게 된다.

 

만약 es6의 문법을 그대로 들고가고 싶다면

 

tsc .\01_hello.js --target es6

다음 명령어를 사용하면 된다.

 

 

//01_hello.ts
var hello = "hello";
let hello2 = "hello2";
let timeoutPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("1 sec");
  }, 1000);
});

timeoutPromise.then(console.log);

다음과같이 Promise를 사용하려고 하면

 

PS C:\Users\user\Desktop\Git\front_end_study\8_TypeScript> tsc .\01_hello.ts             
01_hello.ts:4:26 - error TS2585: 'Promise' only refers to a type, but is being used as a value here. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later.

4 let timeoutPromise = new Promise((resolve, reject) => {
                           ~~~~~~~


Found 1 error.

위와같은 오류가 뜨는데, 이를 해결하려면 라이브러리 옵션을 주면 된다.

 

 

tsc .\01_hello.ts --lib es5,es2015.promise,es2015.iterable,dom

 

 

물론 구현할때마다 일일히 터미널에 칠 필요는 없다.

 

tsconfig.json

 

위 이름의 파일을 하나 생성해주자.

 

 

{
  "include": ["src/**/*.ts"],
  "exclude": [
    "node_modules" //node 패키지들은 컴파일 제외
  ],
  "compilerOptions": {
    "module": "commonjs",
    "rootDir": "src",
    "outDir": "dist",
    "target": "ES5",
    "sourceMap": true, // typescript를 개발자옵션에서 확인가능
    "noImplicitAny": true //타입을 정해야지만 가능하게
  }
}

 

방금 옵션으로 넣어주었던 내용들을 위와같이 파일로 미리 저장해둬서 사용할 수 있다.

 

이제 터미널에서 

 

tsc

 

만 입력해도

 

파일이 잘 생성된것을 알 수 있다.

 

 

 

변수선언

 

var : var키워드는 함수단위 블록스코프이다. 

 

const : 블록스코프를 가지게 된다.

 

const의 경우 변수를 설정하면 타입이 정해지기 때문에

 

function outer() {
    let score = 0;
    score = 30;
    score = '30' // error
}

 

값이 정해지면 타입이 정해지게 된다.

 

function outer() {
    let score;
    score = 30;
    score = '30' 
}

하지만 값이 주어지지 않으면 type이 any가 되기 때문에 에러가 발생하지 않는다.

 

 

function outer() {
    let score : number;
    score = 30;
    score = '30' // error
}

 

: number로 타입을 지정해줄 수도 있다.

 

 

var와 let의 차이를 보려면 밑의 코드를 한번 보자

 

for ( let i =0; i< 3; i++) {
        setTimeout(function() {
            console.log(i); //0,1,2
        },100)
    }

for ( var i =0; i< 3; i++) {
	setTimeout(function() {
		console.log(i); // 3 3 3
	},100)
}

비슷해 보이는 코드이지만,  출력결과가 다르다. 

 

이는 var는 함수단위 스코프이기 때문이다.

 

 

 

const

const는 상수형으로 값을 변경할 수 없다.

 

function outer() {
    const score : number = 100;
    score = 30;  //error

}

 

이때 굳이 :number를 사용하지 않아도 값을 넣음으로서 타입이 정해지기 때문에 생략해도 무방하다.

 

 

타입스크립트의 기본타입

 

number

string

boolean

undefined

null

 

5개의 자바스크립트 기본타입과

 

object

객체와

 

symbol

es5부터 생긴 총 7개의 기본타입을 지원한다.

 

 

 

 

undefined와 null은 모든 타입의 상위타입이다.

 

let numValue : number;

numValue = null;

위와같이 덮어씌우는 식으로 값을 넣어주는것이 가능하다.

 

 

 

symbol

 

symbol이라는 함수타입으로만 생성이 가능하다.

 

 

 

 

 

배열

배열같은 경우는 아래와 같은 방식으로 선언 가능하다.

let nameList : string[]

string말고 다른 타입도 다 가능하다!

 

 

 

객체를 inline타입으로 정해줄 수 있다.

let user1 : {name : string, score : number };
user1 = {
    name : 'mingyu',
    score : 30
}

위처럼 타입을 저장해놓으면 그외에는 사용할 수 없다.

 

let user1 : {name : string, score : number };
user1 = {
    name : 'mingyu',
    score : 30,
    min : 20 //error
}

 

 

 

 

튜플

튜플은 배열과 유사하다

 

let tuple2 : [number, string];

tuple2 = [] //error
tuple2 = [1,2] //error
tuple2 = [1, 'mingyu']
tuple2 = [1,'mingyu',2] //error

튜플안의 개수와 key의 자료형이 모두 맞아야지만 사용이 가능하다.

 

 

 

 

 

interface

 

기본 자료형이 아닌, 하나의 타입을 만드는 방법이다.

 

interface TV {
    turnOn() : boolean; //return 값 설정
    turnOff() : void;
}

const myTV: TV = {
    turnOn(){
        return true;
    },
    turnOff(){

    }
};

function tryTurnOn(tv:TV) {
    tv.turnOn();
}

tryTurnOn(myTV)

다음과 같이 사용할 수 있다.

 

 

인터페이스를 잘 이용하면 깔끔한 코드를 만들기 쉽다.

 

 

예를들어 보드를 만드는 코드를 한번 보자.

 

interface Cell {
  row: number;
  col: number;
  piece?: Piece; //Piece는 있을수도있고 없을수도있다.
}

interface Piece {
  move(from: Cell, to: Cell): boolean;
}

function createBoard() {
  const cells: Cell[] = [];
  for (let row = 0; row < 4; row++) {
    for (let col = 0; col < 3; col++) {
      cells.push({
        row,
        col,
      });
    }
  }
  return cells;
}

const board = createBoard();
board[0].piece = {
  move(from: Cell, to: Cell) {
    return true;
  },
};

 

 

중요한건 타입을 지정해놓으면 함수의 Props로 넘겨주거나 할때 그 타입을 꼭 지켜야지만 정상동작하게 된다는 것이다.

 

 

 

 

함수형 타입

 

function add (x : number, y : number) : number {
    return x + y;
}

//add(1, "2") //error

Props뿐 아니라 반환하는 형도 type을 지정해줘야한다. 명시적으로 표시하지 않으면 추정해서 넣어지게 된다.

 

function add(x: number, y: number): number {
  return x + y;
}

//add(1, "2") //error

function buildUserInfo(name = "-", email?: string) {
  return { name, email }; // name : name, email : email
}

const user = buildUserInfo();  //-, undefind
const user2 = buildUserInfo("mingyu"); // mingyu, undefind

방금 배운 ?와 초기값을 설정해서도 사용할 수 있다.

 

const add2 = ( a: number, b:number) => a+b;

화살표 함수를 써도 잘 적용된다!

 

 

 

함수의 타입과 반환형을 미리 표현해놓는 오버로드 시그니처를 이용할수도 있다.

 

interface Storage {
  a: string;
}
interface ColdStorage {
  b: string;
}

//함수 오버로드 시그니처
function store(type: "통조림"): Storage;
function store(type: "아이스크림"): ColdStorage;

function store(type: "통조림" | "아이스크림") {
  if (type === "통조림") {
    return { a: "통조림" };
  } else if (type === "아이스크림") {
    return { b: "아이스크림" };
  } else {
    throw new Error("에러 발생!!");
  }
}

const s = store("아이스크림");
console.log(s.b); // 아이스크림

다음과 같이 오버로드 시그니처를 미리 만들어둔 후에, store를 실제로 불러내서 사용할 수 있다.

 

type으로 위와같이 문자열을 지정해두면, 지정한 문자만 입력으로 받을 수 있게된다.

 

 

enum

열거형이다.

 

 

enum StarbuksGrade {
  WELCOME,
  GREEN,
  GOLD,
}

function getDiscount(v: StarbuksGrade): number {
  switch (v) {
    case StarbuksGrade.WELCOME:
      return 0;
    case StarbuksGrade.GREEN:
      return 5;
    case StarbuksGrade.GOLD:
      return 10;
  }
}

console.log(getDiscount(StarbuksGrade.GREEN)); // 5
console.log(StarbuksGrade.GREEN); //1
console.log(StarbuksGrade);
// {
//     '0': 'WELCOME',
//     '1': 'GREEN',
//     '2': 'GOLD',
//     WELCOME: 0,
//     GREEN: 1,
//     GOLD: 2
//   }

console.log(StarbuksGrade["0"]) //WELCOME

다음과같이 사용할 수 있다. 이때 만약 WELCOME과 GREEN사이에 값이 넣어지면 함수가 다 꼬여질 수도 있다.

 

enum StarbuksGrade {
  WELCOME = 0,
  GREEN = 1,
  GOLD = 2,
}

다음과같이 순서를 미리 지정해두면 이를 막을 수 있다.

 

 

enum StarbuksGrade {
  WELCOME = "WELCOME",
  GREEN = "GREEN",
  GOLD = "GOLD",
}

function getDiscount(v: StarbuksGrade): number {
  switch (v) {
    case StarbuksGrade.WELCOME:
      return 0;
    case StarbuksGrade.GREEN:
      return 5;
    case StarbuksGrade.GOLD:
      return 10;
  }
}

console.log(getDiscount(StarbuksGrade.GREEN)); // 5
console.log(StarbuksGrade.GREEN); //GREEN
console.log(StarbuksGrade); //{ WELCOME: 'WELCOME', GREEN: 'GREEN', GOLD: 'GOLD' }

위처럼 숫자말고 문자열로 지정해둘수도 있다! 자주 사용되는 방식이다.

 

 

 

클래스

 

클래스도 당연히 타입을 지정해가면서 사용할 수 있다.

 

interface User {
  name: String;
}
interface Product {
  id: string;
  price: number;
}

class Cart {
  protected user: User;
  private store: object;
  constructor(user: User) {
    this.user = user;
    this.store = { id: "-", price: 0 };
  }
  put(id: string, product: Product) {
    this.store = product;
  }
  get(id: string) {
    return this.store;
  }
}



const cartMingyu = new Cart({ name: "mingyu" });
//console.log(cartMingyu.store); //error private이기때문
const cartJeong = new Cart({ name: "Jeong " });

 

 

상속도 가능하다. 추가적으로 말하자면 상속된 클래스도 privated엔 접근이 불가능하며 protected는 접근이 가능하다.

 

class PromotionCart extends Cart {
    addPromotion(){
        //console.log(this.store) //error
        console.log(this.user)
    }
}


const cart2 = new PromotionCart({ name : 'mingyu '});
cart2.addPromotion //'mingyu

 

 

아래처럼 생성자를 만들면서 private와같은 속성까지 지정하는것도 가능하다.

constructor(protected user: User) {
    this.user = user;
    this.store = { id: "-", price: 0 };
  }

 

 

이번엔 좀 전에 배운 interface로 타입을 지정해서 클래스를 만들어보자.

 

interface Person {
  name: string;
  say(message: string): void;
}
interface Programmer {
  writeCode(requirment: string): string;
}

class K_Programmer implements Person, Programmer {
  //Korean은 Person을 보장
  constructor(public name: string) {}
  writeCode(requirment: string): string {
    console.log(requirment);
    return requirment + ".....";
  }
  say(message: string): void {
    console.log(message);
  }

  loveKimchi() {
    console.log("I like kimchi");
  }
}

const mingyu = new K_Programmer("mingyu");

 

조금 구조가 복잡해졌지만 보면 이해하기 어렵지는 않다. 

 

implements를 이용해서 만든 interface를 보장한다고 명시한점만 유의있게 보면 될 것 같다.

 

 

추상클래스를 활용한 조금더 복잡한 코드를 보자.

추상클래스는 오버로드 시그니처처럼 미리 타입들을 지정해둔 클래스를 의미한다. 추상클래스만 선언한 후에 불러내서 사용할 수는 없다.

 

 

interface Person {
  name: string;
  say(message: string): void;
}
interface Programmer {
  writeCode(requirment: string): string;
}

abstract class Korean implements Person {
  public abstract jumin: number;
  constructor(public name: string) {}

  say(message: string): void {
    console.log(message);
  }

  abstract loveKimchi(): void;
}

class K_Programmer extends Korean implements Programmer {
  //Korean은 Person을 보장
  loveKimchi() {
    console.log("I like kimchi");
  }

  constructor(public name: string, public jumin: number) {
    super(name);
  }
  writeCode(requirment: string): string {
    console.log(requirment);
    return requirment + ".....";
  }
  say(message: string): void {
    console.log(message);
  }
}


const mingyu = new K_Programmer('mingyu', 1111);
//const mingyu2 = new Korean('mingyu') //error

Korean 이란 추상클래스를 만든후에, K_Programmer란 클래스에 상속을 해서 사용한 것을 알 수 있다.

 

 

제네릭

타입의 파라미터화를 하는것이다.

 

간단한 예시를 먼저 보자

 

function createPromise<T>(x: T, timeout: number) {
  return new Promise((resolve: (v: T) => void, reject) => {
    setTimeout(() => {
      resolve(x);
    }, timeout);
  });
}

createPromise(1, 100) //type이 number로표시
  .then((v) => console.log(v));

createPromise<string>('1', 100) //type이 string으로표시
  .then((v) => console.log(v));

다음처럼 x의 타입을 T로 넣어주면 알아서 타입이 정해지는것을 알 수 있다.

 

function createPromise<T>(x: T, timeout: number) {
  return new Promise<T>((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, timeout);
  });
}

Promise에도 T로 넘겨주면 굳이 resolve옆에 타입을 적지 않아주어도 된다.

 

 

타입을 파라미터화 해서 타입을 유지하면서 코드를 작성하게 하는 방식이란걸 생각하면 될 것 같다. 아래는 튜플로 반환해보는 예시이다.

function createTuple2<T, U>(v: T, v2: U): [T, U] {
  //튜플로 반환
  return [v, v2];
}

const t1 = createTuple2("mingyu", 25); //2개의 튜플이 저장

 

 

 

제네릭은 클래스를 정의할때도 사용된다.

 

class LocalDB {
  constructor(private localStorageKey: string) {}
  add(v: User) {
    localStorage.setItem(this.localStorageKey, JSON.stringify(v));
  }
  get() :User {
    const v = localStorage.getItem(this.localStorageKey);
    return v ? JSON.parse(v) : null;
  }
}
interface User {
  name: string;
}

const userDb = new LocalDB("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : string

 

위코드를 제네릭을 적용하면

 

class LocalDB<T> {
  constructor(private localStorageKey: string) {}
  add(v: T) {
    localStorage.setItem(this.localStorageKey, JSON.stringify(v));
  }
  get() :T {
    const v = localStorage.getItem(this.localStorageKey);
    return v ? JSON.parse(v) : null;
  }
}
interface User {
  name: string;
}

const userDb = new LocalDB<User>("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : any

이처럼 된다. 즉, 타입을 파라미터처럼 사용할수 있게 된 것이다.

 

interface에서도 제너릭을 사용할 수 있다.

 

 

interface DB<T>{
    add(v:T) : void;
    get() : T;
}

class D<T> implements DB<T> {
    add(v: T): void {
        throw new Error("Method not implemented.");
    }
    get(): T {
        throw new Error("Method not implemented.");
    }    
}


class LocalDB<T> implements DB<T> {  //<T>가 이어지면서 전달된다.
  constructor(private localStorageKey: string) {}
  add(v: T) {
    localStorage.setItem(this.localStorageKey, JSON.stringify(v));
  }
  get() :T {
    const v = localStorage.getItem(this.localStorageKey);
    return v ? JSON.parse(v) : null;
  }
}
interface User {
  name: string;
}

const userDb = new LocalDB<User>("mingyu");
userDb.add({ name: "mingyu" });
const user1 = userDb.get();
user1.name; //type : any

대신 interface를 사용해서 쓸때 class이름에도 제너릭을 가져와야 사용이 가능함을 알 수 있다.

 

 

 

특정 type의 high-type으로써 제너릭을 사용할수도 있다. 범위를 고정시킨다고 볼수도 있을 것 같다.

 

interface JSONSerialier {
    serialize() : string;
}


class LocalDB<T extends JSONSerialier> implements DB<T> {  //<T>가 이어지면서 전달된다.
  constructor(private localStorageKey: string) {}
  add(v: T) {
    localStorage.setItem(this.localStorageKey, JSON.stringify(v));
  }
  get() :T {
    const v = localStorage.getItem(this.localStorageKey);
    return v ? JSON.parse(v) : null;
  }
}
interface User {
  name: string;
}

 

 

제너릭에서 조건문 타입도 사용할 수 있다.

 

interface Veigtable {
  v: string;
}
interface Meat {
  m: string;
}
interface Cart2<T> {
  getItem(): T extends Veigtable ? Veigtable : Meat;
}
// const cart1 : Cart2<string> ={   //error
//     getItem() {
//         return ''
//     }
// }
const cart2: Cart2<string> = {
  getItem() {
    return { m: "" };
  },
};
// const cart3 : Cart2<Veigtable> ={ //error
//     getItem() {
//         return { m : ""}
//     }
// }

하나의 타입뿐이 아닌 여러개의 타입에 따라서 동작이 달라지는것을 확인할 수 있다.

 

 

 

 

Intersection 

 

interface User {
  name: string;
}
interface Action {
  do(): void;
}

function createUserAction(u: User, a: Action): User & Action { //intersection
  return { ...u, ...a };
}

const u = createUserAction({ name: "mingyu" }, { do() {} });
//굳이 UserAction이라던가 하는 interface를 만들필요가 없다!

두 인터페이스를 합쳐서 반환하는 예제이다

 

 

Union Types 

function compare(x: string | number, y: string | number) {
  if (typeof x === "number" && typeof y === "number") {
    return x === y ? 0 : x > y ? 1 : -1;
  }
  if (typeof x === "string" && typeof y === "string") {
    return x.localeCompare(y); //같으면 0 크면1 작으면 -1
  }
  throw new Error("not supported type");
}

const v = compare("a", "b");
const w = compare("a", 1); //error but 명시적으로 표시안됨

이때 w에 오류를 출력하는걸 명시적으로 표시하고 싶다면 좀전에 배운 오버로드 시그니처를 사용하면 된다!

 

function compare(x: string, y: string): number;
function compare(x: number, y: number): number;
function compare(x: string | number, y: string | number) {
  if (typeof x === "number" && typeof y === "number") {
    return x === y ? 0 : x > y ? 1 : -1;
  }
  if (typeof x === "string" && typeof y === "string") {
    return x.localeCompare(y); //같으면 0 크면1 작으면 -1
  }
  throw new Error("not supported type");
}

const v = compare("a", "b");
//const w = compare("a", 1); //error

 

 

만약 interface로 만든 type이라면 typeof를 사용하여 if문을 구현할 수 없다. 아래와같이 Action타입임을 확실시하게 만들어서 코드를 짜는 방법이 있긴하다.

interface User {
    name: string;
  }
  interface Action {
    do(): void;
  }


function process ( v : User | Action ) {
    if((<Action>v).do) {
        (<Action>v).do
    }  
}

 

별도의 타입가드를 아래와같이 정의해두면 조금 더 편리하게 쓸 수 있다.

 

interface User {
  name: string;
}
interface Action {
  do(): void;
}

function isAction(v: User | Action): v is Action {
  //타입가드 정의
  return (<Action>v).do !== undefined; //v안에 do가있다면 Action이다
}

function process(v: User | Action) {
  if (isAction(v)) {
    v.do();
  } else {
    console.log(v.name);
  }
}

 

 

 

타입 별칭

 

타입의 이름을 바꾸거나 별칭을 붙이는데 사용된다.

 

interface User {
    name: string;
  }
  interface Action {
    do(): void;
  }

type UserAction = User & Action;
function createUserAction() :UserAction {
    return {
        do() {},
        name : ''
    }
}

type StringOrNumber = string | number;
type Arr<T> = T[]; 
type P<T> = Promise<T>;

 

 

물론 interface처럼 바로 선언하는 것도 가능하다.

 

 

type User2 = {
  name: string;
  login(): Boolean;
};

class UserImpl implements User2 {
  name: string;

  login(): boolean {
    throw new Error("에러 발생!");
  }
}

 

type을 사용하면 문자열을 type으로 바로 사용할때 유용하다.

 

type User2 = {
  name: string;
  login(): Boolean;
};

class UserImpl implements User2 {
  name: string;

  login(): boolean {
    throw new Error("에러 발생!");
  }
}

type UserState = "PENDING" | "APPROVED" | "REJECTED";

function checkUser(user: User2): UserState {
  if (user.login()) {
    return "APPROVED";
  } else {
    return "REJECTED";
  }
}

UserState type을 손쉽게 구현한것을 볼 수 있다.

 

 

 

인덱스타입

 

속성의 이름이 정의되어 있지않고 동적일때 사용가능하다

 

interface Props {
  [key: string]: string;
  // [key : boolean] : string ; //error 인덱스타입은 number나 string만

  name: string;
}

const p: Props = {
  name: "mingyu", //필수
  a: "d",
  b: "e",
  //c : 3  //error
  1: "b", //error가 나지 않음! p[1]로 조회가능
};

 

어떤 값이 오든 동적으로 처리할 수 있다.

 

 

keyof Props를 사용해서 key의 타입을 가져오는것도 가능하다.

 

let keys : keyof Props;

interface User {
    name : string;
    age : number;
    hello(msg : string) : void;
}

let keysofUser : keyof User;
keysofUser ="age"

let helloMethod : User["hello"]
helloMethod = function (msg : string) {
    
}
// helloMethod = function (msg : number) { //error
    
// }

 

 

이렇게 타입스크립트 언어의 문법을 조금 더 자세히 알아보았다

728x90