본문 바로가기
FE/typescript

[TypeScript] 타입스크립트 2주 챌린지 (노마드코더)

by s0ojin 2023. 4. 28.

이 글은 노마드코더 챌린지 진도에 맞게 노마드코더 Typescript로 블록체인 만들기 강의를 듣고 그 내용을 정리한 것입니다. 챌리지의 과제는 노마드코더 챌린지 규칙상 공유드릴 수 없음을 알려드립니다.

1. Introduction

누가 타입스크립를 사용해야할까?


  • 더 나은 생산성과 더 나은 사용자 경험을 원하는 Javascript 개발자
  • 타입 사용에 익숙한 Java, C 등을 사용하는 개발자

왜 자바스크립트는 문제인가?


1. 자바스크립트는 매우 유연한 언어이다. ⇒ 개발자가 잘못된 코드를 입력해도 최대한 이해하려고함.

  • []+false ⇒ string
  • 함수에서 인자를 하나만 보내도 실행됨.
function devide(a, b) {
	return a / b
}

divide(2, 3) // 0.66666666666666
divide('XXXXXXXX') //NaN
  • 객체에 존재하지 않는 함수도 호출할 수 있음.
const nico = { name:'nico' }

nico.hello() //Uncaught TypeError: nico.hello is not a function

  1. 그래서 코드를 실행하기 전까지는 에러를 알 수 없다. 실행 후 에러가 발생하는 문제 빈번 (런타임에러) → 유저가 직접 에러를 마주하게 됨.

2. Overview of Typescript

  • 코드를 실행하기 전에 에러를 잡고싶다!
  • 타입스크립트는 자바스크립트로 변환된다. 타입스크립트가 에러를 감지하면 javascript로 컴파일 되지 않는다.

Implicit Types vs Explicit Types


  • 타입스크립트가 저절로 추론하여 타입을 정한다.
  • 명시적으로 타입을 알려준다.
    • 대개의 경우 타입스크립트가 타입을 추론하도록 두는 것은 좋다. 그러나 때로 직접 명시해줘야할 때가 있음
      const c = [1, 2, 3]
      c.push("1") // 이미 c가 숫자들을 담는 배열임을 추론하고 있으므로 저절로 오류 발생시킴
      
      const c : number[] = []
      c.push("1") //빈배열일 경우 직접 명시해주어야함.

Types of Typescript part.I


  1. 객체 타입 선언하기, 선택적으로 사용하고 싶으면 type뒤에 ?붙이면 됨.
const nicoPlayer : {
	name:string,
	age?:number
} = {
	name: "nico"
}

  1. type Alias(별칭)

    : 1번에서 player가 여러명이라면 똑같으 타입 코드를 반복해서 작성해야한다. 이럴 때 Alias를 사용하면 코드를 재사용해 간결하게 할 수 있다.

type Player = {
	name:string;
	age?:number;
}


const nico : Player = {
	name: "nico"
}

const sujin : Player = {
	name: "sujin",
	age: 27
}

  1. 함수 타입

// string타입의 name 인수를 받아 Player 타입을 리턴하는 함수
function playerMaker(name:string):Player {
	return {
		name: name
	}
}

//화살표함수
const playerMaker => (name:string):Player =>({name:name})

Types of Typescript part.II


  1. readonly : 변경 불가능
type Player = {
	readonly name:string;
	age?:number;
}

const numbers : readonly number[] = [1, 2, 3, 4];
numbers.push(1) //Error발생

  1. Tuple : array를 만듦, 최소한의 길이, 특정위치에 특정타입이 있어야함.
// 이름, 나이, 챔피언 여부 [string, number, boolean]

const player: [string, number, boolean] = ["nico", 20, true];

  1. undefined, null, any
  • 선택적 타입은 undefined가 될 수 있다.
let a : undefined = undefined;
let b : null = null;
  • 비어있는 값을 쓰면 any가 default
  • any는 어떤 타입이든 될 수 있다. Typescript를 빠져나오는 방법. 사용지양

Types of Typescript part.III


  1. unknown : 변수타입을 미리알 수 없을 때 사용. 사용하기 전에 타입을 확인하도록 보호받을 수 있음.
let a:unknown;

let b = a + 1; //Error

if (typeof a === 'number') {
	let b = a + 1;
}

  1. void : 아무것도 return하지않는 함수. 대개는 void를 직접 지정할 필요는 없음.

  1. never
    1. 함수가 절대 return하지 않을 때 발생. (가령 오류를 내뱉는 함수처럼)
      function hello():never {
      	reutrn "X";
      } //Error
      
      function hello():never {
      	throw new Error("xxx");
      }
    1. 타입이 두가지일 수도 있는 상황에서 발생.
      function hello(name:string|number) {
      	if( typeof name === "string") {
      			// name type string
      	} else if (typeof name === "number") {
      			// name type number
      	} else {
      		name // name type never
      	}
      }

3. Functions

Call signature


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

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

Overloading


: 여러개의 서로다른 call signature를 갖고 있는 함수

: overloading 된 함수를 작성할일은 많지 않지만 외부 패키지, 라이브러리에서 굉장히 많이 사용.

type Add = {
	(a:number, b:number) : number;
	(a:number, b:string) : number;
}

  1. Next.js 예시 - 둘다 작동함
Router.push("/home");

Router.push({
	path: "/home",
	state: 1
});


//type
type Config = {
	path: string,
	state: object
}

type Push = {
	(path:string):void
	(config:Config):void
}

const push:Push = (config) => {
	if(typeof config === "string") console.log(config)
	else {
		console.log(config.path, config.state)
	}
}

  1. 파라미터의 갯수가 다른 경우
type Add = {
	(a:number, b:number):number,
	(a:number, b:number, c:number):number
}

const add:Add = (a, b, c?:number) => {
	if(c) return a + b + c
	return a + b
}

Polymorphism(다형성) : Generic(제네릭)


: many + structure

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

const superPrint:SuperPrint = (arr) => {
	arr.forEach(i => console.log(i))

superPrint([1,2,3,4])
superPrint([true,false,true])
superPrint(["a","b","c"])

superPrint([1,2,true,false])//Error

  • 제네릭(Generic)이란 type의 placeholder 같은 것. call signiture를 작성할 때 들어올 타입을 정확히 모를때 사용하면, 타입스크립트가 상황에따라 타입을 유추한다.
  • 대문자로 시작한다면 어떤 이름이라도 상관없음.
type SuperPrint = {
	<T>(arr:T[]):void
}

const superPrint:SuperPrint = (arr) => {
	arr.forEach(i => console.log(i))
}

superPrint([1,2,3,4])
superPrint([true,false,true])
superPrint(["a","b","c"])
superPrint([1,2,true,false])

  • 제네릭을 여러개 사용할 경우
type SuperPrint = {
	<T,M>(arr:T[], b:M):void
}

  • 제네릭 확장예시
type Player<E> = {
	name: string
	extraInfo: E
}

type NicoPlayer = Player<{favFood:string}>

const nico:NicoPlayer = {
	name: "nico",
	extraInfo: {
		favFood: "kimchi"
	}
}

const lynn:Player<null> = {
	name: "lynn",
	extraInfo: null
}

  • 다양한 제네릭의 활용 : 타입스크립트의 타입들은 대부분 제네릭으로 선언되어있다. ex) Array<T>, useState<T> number[] == Array<number> useState<number>()

4. Classes and Interfaces

Classes


  1. 타입스크립트로 클래스 선언하기
    class Player {
    	constructor(
    		private firstName:string,
    		private lastName:string,
    		public nickname:string
    	) {}
    }
    
    const nico = new Player("nico", "las", "니꼬");
    
    nico.firstName //Errror : firstName is private // 자바스크립트에선 에러 안남

  1. Abstract class(추상클래스) **Typescript 기능, Javascript에 없음

    : 다른 클래스가 상속받을 수 있는 클래스, 직접 새로운 인스턴스를 만들 수는 없음

    abstract class User {
    		constructor(
    		private firstName:string,
    		private lastName:string,
    		public nickname:string
    	) {}
    
    	getFullName() {
    		return `${this.firstName} ${this.lastName}`
    	}
    	private getFirstName() {
    		return this.firstName
    	} //private나 public은 property뿐만 아니라 method에서도 작동한다.
    }
    
    class Player extends User {}
    
    const nico = new Player("nico", "las", "니꼬");
    
    nico.getFullName();
    nico.getFirstName(); //Errror : getFirstName is private

  1. Abstract method(추상 메소드) **Typescript 기능, Javascript에 없음

    : 추상클래스를 상속받는 모든 것들이 구현해야하는 메소드를 의미

    abstract class User {
    		constructor(
    		-private +protected  firstName:string,
    		private readonly lastName:string, // readonly : 읽을 순 있지만 수정은 안됨.
    		public nickname:string
    	) {}
    	
    	abstract getFirstName():void
    
    	getFullName() {
    		return `${this.firstName} ${this.lastName}`
    	}
    }
    
    // User를 상속받은 클래스이므로 getFirstName을 구현하지 않으면 에러남
    class Player extends User {
    	getFirstName() {
    		//Player가  User를 상속받았으나 private 필드에는 여전히 접근할 수 없음
    		//firstName을 private대신 protected를 사용하면됨.
    		console.log(this.firstName) //Error
    	}
    } 
    
    const nico = new Player("nico", "las", "니꼬");
    

Interfaces part.I (타입 vs 인터페이스)


  1. type
    1. type은 모든 될 수 있다. 특정값, string, object 등등
    1. property 축적되지 않음
    type Team = "red" | "blue" | "yellow";
    type Health = number;
    
    type Player = {
    	nickname: string,
    	team: Team,
    	health: Health
    }
    
    const nico :Player = {
    	nickname: "nico",
    	team: "pink", //Error : Team에서 정한 특정값만을 가져야함.
    	health: 10
    }
    
    //상속
    type User = {
    	name:string;
    }
    
    type Player = User & {
    }
    
    const nico:Player = {
    	name: "nico"
    }

  1. interfaces
    1. object나 class 모양을 특정할 때 사용.
    1. 상속시 클래스와 유사한 인터페이스
    1. property가 축적됨
    type Team = "red" | "blue" | "yellow";
    type Health = number;
    
    interface Player {
    	nickname: string,
    	team: Team,
    	health: Health
    } 
    
    const nico :Player = {
    	nickname: "nico",
    	team: "pink", //Error : Team에서 정한 특정값만을 가져야함.
    	health: 10
    }
    
    
    //상속
    interface User {
    	name:string;
    }
    
    interface Player extends User {
    }
    
    const nico:Player = {
    	name: "nico
    }
    
    //property 축적
    interface User {
    	name:string;
    }
    interface User {
    	lastName:string;
    }
    interface User {
    	health:number;
    }
    
    const nico: User = {
    	name: "nic0",
    	lastName: "ge",
    	health:10
    }

Interfaces part.II (추상클래스 대신 인터페이스 사용하기)


  1. 추상클래스
    abstract class User {
    	constructor(
    		protected firstName:string,
    		protected lastName:string
    	) {}
    
    	abstract sayHi(name:string):string
    	abstract fullName():string
    }
    // User를 상속받는 클래스는 sayHi와 fullName을 반드시 구현해야함
    // firstName과 lastName을 갖고있음
    
    
    
    class Player extends User {
    	fullName() {
    		return `${this.firstName} ${this.lastName}`
    	}
    	sayHi(name:string) {
    		return `Hello ${name}. My name is ${this.fullName()}`
    	}
    }
  • 추상클래스는 상속받을 클래스들이 꼭 가져야하는 메서드 등을 정해둘 수 있어 좋지만, 자바스크립트로 컴파일되면 일반 클래스로 바뀌기때문에 의미가 사라짐. 이때 interface를 사용하면 유용. interface는 컴파일되면 사라짐.

  1. 추상클래스 대신 인터페이스로 구현하기
    1. 추상클래스와 동일하게 원하는 property를 가지도록 강제하고 싶음.
    1. 인터페이스를 상속받은 property는 private이나 protected사용불가
interface User {
	protected firstName:string,
	protected lastName:string,
	sayHi(name:string):string
	fullName():string
}

interface Human {
	health:number
}

class Player implements User, Human {
	constructor(
		public firstName:string,
		public lastName:string,
		public health:number
	) {}

	fullName() {
		return `${this.firstName} ${this.lastName}`
	}
	sayHi(name:string) {
		return `Hello ${name}. My name is ${this.fullName()}`
	}
}

  1. 추상클래스 대신 타입으로 구현하기 : 인터페이스 방식과 동일
    type PlayerA = {
    	firstName:string;
    }
    
    class User implements PlayerA {
    	constructor(
    		public firstName:string
    	){}
    }

Polymorphism(다형성)


: 다른모양의 코드를 가질 수 있게 해 주는 것 → 제네릭 사용

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

class LocalStorage<T> {
    private storage:IStorage<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 stringsStorage = new LocalStorage<string>()
stringsStorage.get("ff")

const booleanStorage = new LocalStorage<boolean>()
booleanStorage.get('ssss')

5 [2022 UPDATE] TYPESCRIPT BLOCKCHAIN

Targets


// tsconfig.json
{
  "include": ["src"],
  "compilerOptions": {
    "outDir": "build",
    "target": "ES6"
  }
}
  • include 배열: 자바스크립트로 컴파일하고 싶은 모든 디렉토리
  • outDir: 자바스크립트 파일이 생성될 디렉토리
  • target: 컴파일될 ES 버전

Lib Configuration


  • lib: 합쳐진 라이브러리의 정의 파일을 특정해주는 역할
  • 코드가 어디서 동작할 것인지 알려줄 수 있음. ex) “DOM”을 입력하면 브라우저API 자동완성 사용가능
// tsconfig.json
{
	...

    "lib": ["ES6", "DOM"]
  }
}

Declaration Files


  • strict mode를 설정하면 엄격한 타입검사 옵션이 활성화되고, d.ts 파일이 없는 경우도 에러 발생시킴.
// tsconfig.json
{
	...

    "strict": true
  }
}
  • 자바스크립트 라이브러리들의 타입을 정의하는 파일 d.ts
//myPackage.js : 외부 라이브러리라고 가정

export function init(config) {
  return true;
}

export function exit(code) {
  return code + 1;
}
//myPackage.d.ts : myPackage의 타입 선언

interface Config {
  url: string;
}

declare module "myPackage" {
  function init(config: Config): boolean;
  function exit(code: number): number;
}

  • lib.d.ts: 타입스크립트의 기본적인 타입 정의를 포함하고 있음.

JSDoc


  • allowJS : ts와 js를 혼용해서 사용할 경우 사용.
    // tsconfig.json
    {
    	...
    
        "strict": true,
    		"allowJs" : true
      }
    }

  • js에서 ts 마이그레이션할 때 ts로 바꾸기 곤란한 js파일의 타입을 체크하고 싶다면? →//@ts-check : type error 발생시켜줌
    //myPackage.js
    
    // @ts-check
    export function init(config) {
      return true;
    }
    
    export function exit(code) {
      return code + 1;
    }

  • js에서 ts 마이그레이션할 때 ts로 바꾸기 곤란한 js파일의 타입 보호를 받고싶다면? → JSDoc : /** */ 안에 작성
    // @ts-check
    
    /**
     * initializes the project
     * @param {object} config
     * @param {boolean} config.debug
     * @param {string} config.url
     * @returns {boolean}
     */
    export function init(config) {
      return true;
    }
    
    /**
     * Exits the program
     * @param {number} code 
     * @returns {number}
     */
    export function exit(code) {
      return code + 1;
    }


Uploaded by N2T

'FE > typescript' 카테고리의 다른 글

[Typescript] AxiosError의 response받아오기, AxiosError Type  (0) 2023.02.26

댓글