타입스크립트 문법 정리

707·2022년 6월 9일
1
post-thumbnail

노마드코더의 타입스크립트 강의를 듣고 정리한 내용입니다.
https://nomadcoders.co/typescript-for-beginners/lobby

💡 정리
  • 명시적으로 타입을 지정해주는 것보다 타입스크립트가 추론하도록 하는게 더 낫다 : 효율적! → 이 부분은 사람마다 스타일이 다른 것 같음. 이번 프로젝트에서는 최대한 명시적으로 지정해주는 식으로 진행
  • type Arr = Array
    type Arr = string[] 이거 두개는 차이가 없음. 뭐가 다른가 하고 찾아봤는데 신경안써도 되는듯?
  • 인터페이스를 타입으로 지정해줄 수도 있음
  • 상속이 좀 헷갈림.. (❗️이거 이번 프로젝트에서 되게 유용하게 참고했음)
    - 인터페이스의 인터페이스 상속 : interface playerAA extends playerA {} : 이건 js문법
    - 클래스의 인터페이스 상속 : class player AA implements playerA {} : 이건 ts문법
    - 타입의 타입 상속 : type playerAA = playerA & {}
    - 클래스의 클래스 상속 : class playerAA extends playerA {}ㅠ

🌴 Type

타입 설정하기 (1)

  • optional type (객체내에서 해당 변수를 포함할수도 안할수도 있다는 걸 ? 로 나타냄)
// error
const player : {
	name: string,
	age: number
} = {
	name: 'hanbin',
}

// correct
const player : {
	name: string,
	age?: number // age : number | undefined
} = {
	name: 'hanbin',
}

// 👉 요렇게 오류를 잡아준다!
// error : age가 없으면?
if (player.age < 10) {}

// correct
if (player.age && player.age < 10) {} // age가 없는 경우에 대한 오류발생가능성 차단
  • 그렇다면 같은 구조의 객체생성할때마다 매번 타입을 지정해줘야할까? ⇒ alias(별칭)타입을 생성하면됨
type Player = {
	name: string,
	age?: number
} // ❗️타입의 첫글자는 대문자로!

const playerHan : Player = {
	name: 'hanbin'
}

// 객체가 아닌 다른 데이터타입도 type을 이용해 설정이 가능하다
type Age = number;
type Player = {
	name: string,
	age?: Age
} // ❗️타입의 첫글자는 대문자로!

const playerHan : Player = {
	name: 'hanbin'
}
  • 함수에서 리턴값의 타입 지정해주기
// error (age를 추가하려고 하면 에러 발생!)
function playerMaker(name: string) {
	return {
		name
	}
}
const hanbin = playerMaker('hanbin')
hanbin.age = 29 // ❌

// correct (return되는 객체의 type이 위에서 지정한 Player 임을 명시해줌)
function playerMaker(name: string): Player {
	return {
		name
	}
}
const hanbin = playerMaker('hanbin')
hanbin.age = 29 // optional로 지정해준 age가 들어오면 에러없이 잘 동작

타입 설정하기 (2)

  • Readonly 속성
type Player = {
	readonly name: Name,
	age?: Numbber
}
// 읽기전용으로 만들고 싶으면 앞에 readonly를 추가! 
// 일반 데이터값이든 배열이든 객체든 그 안의 값이 const 처럼 수정할 수 없게됨
  • Tuple

튜플은 배열을 생성해줌. 최소length값이 있고 특정 위치에 특정 값이 있어야함

cosnt Player : [string, number, boolean] = ['hanbin', 29, true];
// 반드시 3개의 값을 가져야하며, 각 위치별로 정해진 타입과 일치해야함
// readonly를 붙일 수 있음
  • any 타입스크립트의 문법에서 빠져나오고 싶을때. 그냥 자바스크립트처럼 자유롭게 쓰고싶을때 사용하는 데이터타입 안쓰는걸 추천. 정말 필요할 때만 신중하게 쓸것

타입 설정하기 (3)

  • 타입스크립트만의 독특한 타입 : unkown, void, never 타입스크립트의 핵심 = type checker와 소통하는것!
    • unknown

      변수의 타입을 미리 알지 못할 때 (api로 데이터 받아올 때 등)

      let a: unknown;
      
      // error
      let b = a + 1;
      // correct
      if (typeof a === 'number') {
      	let b = a + 1;
      }
      
      //error
      let b = a.toUpperCase();
      
      // correct
      if (typeof a === 'string') {
      	let b = a.toUpperCase
      }

      unknown으로 설정하면 해당 변수를 사용할 때 미리 변수의 데이터타입을 확인해주는 작업을 필요로함

      근데 if문으로 저렇게 쓰면 해당 코드블럭안에서는 오류가 안난다는게 뭔가 신기함. 그냥 변수 선언할 때 타입지정해주는건 당연히 컴퓨터가 알겠거니 하는데 if문으로 타입을 규정해주는걸 코드블럭 안에서 알아듣고 문제없다고 인식한다는게 되게 새로움..

    • void

      리턴값이 없는 함수

    • never

      에러상황에 쓰이는 데이터타입. 신기하네

      function hello (name: string|number) {
      	if (typeof name === 'string'){
      		name // 타입을 확인하면 string으로 뜸
      	} else if (typeof name === 'number') {
      		name // 타입을 확인하면 number로 뜸
      	} else {
      		name // 🔥타입을 확인하면 never로 뜸
      	}
      }
      
      // 위와같이 지정해준 타입과 맞지 않아 일어날 수 없는 상황 => 에러 발생
      // 이런게 never라는 데이터타입을 가짐.
      // 그래서 함수의 리턴값을 never로 지정해주면 그 함수는 return을 할 수 없고 에러처리만 가능함
      function hello():never {
      	// error
      	console.log('X') 
      
      	// correct
      	throw new Error('X')
      }

🌴 Function

Call Signatures

콜시그니쳐 = 함수에 마우스 올리면 위에 나타나는 설명글. 인자나 리턴값의 데이터타입을 명시해줌

이걸 type 으로 직접 지정해줄 수 있음

// call signature 함수의 인자타입, 리턴값타입을 지정
// 함수를 작성하기 전 미리 데이터타입에 대해 생각할 수 있게해준다!
type Add = (a:number, b:number) => number

// ❗️ 콜 시그니쳐를 이렇게도 쓸 수 있다 : 오버로딩
type Add = {
	(a:number, b:number) : number
}

// add라는 함수를 만들때 해당 call signature를 가지고 오면 자동으로 인식함
const add: Add = (a,b) => a+b

리액트를 쓸 때 props로 함수를 보내려고 하면 타입스크립트에게 이 함수가 어떻게 구성된건지 알려줘야함. 그럴때 사용.. 근데 이때 정확히 어떻게 코드로 구현되는지는 좀 봐야 알거같음

Overloading

함수가 여러개의 다른 콜시그니쳐를 가지고 있을때 발생됨

실제로 오버로딩을 쓰는 코드를 작성할 일은 많지 않음. 근데 외부 라이브러리 같은거는 오버로딩 많이 써서 얘의 개념이랑 어떻게 동작하는지를 알아둬야함.

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

// 이런게 오버로딩임. 그냥 단순히 콜시그니쳐가 여러개인 함수
// 그래서 함수 내부에서 각 콜시그니쳐의 경우에 대해 데이터타입별 처리를 나눠서 해줘야함
const add:Add = (a,b) => {
	// error. b의 타입이 string일 수도 있어서 number와 더할 수 없음
	return a+b

	//correct
	if (typeof b === 'string') return a // 인자값 조건과 리턴값 타입조건을 모두 만족
	return a+b
}

오버로딩의 예시

// next.js에서 Router를 사용할 때
// 1. string만 담아서 쓸 수 있고
Router.push("/home")
// 2. 이렇게 객체형태로 다른 값을 추가로 담아서 쓸 수도 있음.
Router.push({
	path: "/home",
	state: 1
}

//이런경우에 push함수는 다음과 같은 콜시그니쳐를 가짐
type Config = {
	path: string,
	state: number
}

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

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

// 패키지나 라이브러리를 설계할 때 이런 오버로딩을 많이 사용함
// 위의 경우는 넘겨주는 인자값의 타입이 다를 때 사용한거고

// 아래는 넘겨주는 인자값의 갯수가 다를 때 <= 옵셔널하게 넘기는 인자가 있을 때!
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 & Generics

다양한 타입이 될 수 있는 콜 시그니쳐

함수를 해당 콜시그니쳐로 지정하면 들어오는 인자값에 따라 타입이 정해짐

제네릭은 내가 요구한대로 콜시그니쳐를 만들어 줌

type SuperPrint = {
	<TypePlaceholder>(arr: TypePlaceholder[]): TypePlaceholder
} // 여기서 <여기들어가는이름>은 아무거나 해도 상관없음. 얘가 Generic이 됨. 그래서 다음처럼 많이씀

type SuperPrint = {
	<T>(arr: T[]): T
} // 배열에 있는 거 보고 알아서 타입 유추하고 그 타입 중에 하나를 리턴해

const superPrint : superPrint = (arr) => arr[0]

// 아래가 모두 가능하다. 각각의 콜시그니쳐를 확인해보면 다르다
// 일일이 각각의 경우에 대한 콜시그니쳐를 작성하고 오버로딩이 필요 없음
const a = superPrint([1,2,3,4])
const b = superPrint([true, false, false])
const C = superPrint(['a', 'b', 'c'])
const d = superPrint([1, true, 'a'])

// 함수 말고 변수에 데이터를 저장할 때도 가능
type Player<E> = {
	name: string,
	extraInfo: E
} // 디스패치 보낼 때 액션을 이런 식으로 제네릭으로 데이터타입을 설정하면 좋을듯?

// 1
const hanbin : Player<{favFood: string}> = {
	name: 'hanbin',
	extraInfo: {
	favFood: 'kimchi'
}

// 2
type HanbinPlayer = Player<{favFood: string}>
const hanbin : hanbinPlayer = {
	name: 'hanbin',
	extraInfo: {
	favFood: 'kimchi'
}

// 3
type HanbinExtra = {
	favFood: string
}
type HanbinPlayer = Player<HanbinExtra>

// 리액트에서 이렇게 제네릭을 활용할 수 있음
useState<number>()
useState 함수를 쓸건데 state의 타입을 number로 할꺼다
useState 함수의 콜시그니쳐에게 리턴값의 타입을 명시적으로 지정해줄 수 있게됨

제네릭을 작성할 일은 거의 없음. 다른 사람의 코드를 가지고 와서 제네릭을 쓸 일은 있어도

이거는 많이 써봐야 좀 익숙해질거같은데

🌴 Class와 Interface

Class

class Player  {
    constructor(
        private firstName: string,
        private lastName: string,
        public nickname: string
    ){} // private, public 키워드는 타입스크립트 영역내에서만 작동되는 확인장치같은거
				// 상속받은 클래스 내에서는 사용가능하나 클래스 외부에서 사용되는건 피하고 싶은 경우엔
				// protected 라는 키워드를 사용하면 됨!
}

const hanbin = new Player('bin', 'han', '비니')

// error
hanbin.firstName
hanbin.lastName

// correct : public으로 설정한 것만 클래스 바깥에서 알 수 있음
hanbin.nickname
  • abstract class

다른 클래스가 상속을 받을 수 있는 클래스. 직접 인스턴스를 생성할 수는 없음

abstract class User {
	    constructor(
	        private firstName: string,
	        private lastName: string,
	        public nickname: string
	    ){} // private, public 키워드는 타입스크립트 영역내에서만 작동되는 확인장치같은거
}

class Player extends User {

}

// error
const hanbin = new User('bin', 'han', '비니')

// correct
const hanbin = new Player('bin', 'han', '비니')

abstract method

abstract class User {
  constructor(
      private firstName: string,
      private lastName: string,
      public nickname: string
  ){}
	// abstract class 에서 메소드를 추가할 수도 있고 public과 private 설정도 가능
	private getFullName() {
	return `${this.fi{rstName} ${this.lastName}`
	}`
}

class Player extends User {}
const hanbin = new Player('bin', 'han', '비니')

// error
hanbin.getFullName()

// abstract method란?
// abstract class에서 메소드함수를 생성하는 대신 콜시그니쳐를 작성한 것
// abstract method를 abstract class 안에 만들어두면
// 그 추상클래스를 상속받은 모든 클래스는 추상메소드로 지정해둔 함수를 무조건 구현해야함
// 리액트 컴포넌트의 render 메소드 같은 건가봄

abstract class User {
	constructor(
		private firstName: string,
		private lastName: string,
		public nickname: string
	) {}
	// abstract method : 구현할 메소드의 타입만 지정해줌
	abstract getFullName(): string
}

class player extends User{
	getFullName () {
			return `${this.firstName} ${this.lastName}`
	}
}
const hanbin = new Player('bin', 'han', '비니')
  • class로 만든 뒤 type을 지정해줄 때 concrete type(string, number, boolean...)이나 generic type 대신에 class로 지정해 줄 수도 있음!

Interface (1)

인터페이스는 타입이랑 비슷하게 쓰면 되는데 다른건 할 수 없고 객체의 형태를 지정해주는 용도로만 쓸 수 있음

type Team = 'red' | 'blue' | 'yellow'
// 이런식으로 type을 사용하면 string이라는 concrete 타입만 지정해줄 수 있는게 아니라 
// 세부적으로 항목을 지정해 줄 수 있음. 
// Team이라는 타입은 'red', 'blue', 'yellow'라는 스트링 중 하나만 가능함

interface Player {
	name: string,
	age: number,
	team: Team
}

인터페이스를 사용한 것이 기존의 객체지향 문법과 비슷해서 더 사용하기 편하다고함.

유의할 점은 인터페이스를 사용하면 객체 내부의 인자는 모두 public이 됨.

인터페이스의 장점은 TS에서 JS로 변환될 때 abstract class는 그대로 class로 변환되지만 interface는 타입스크립트에서만 존재하고 자바스크립트에서는 아무런 코드가 나타나지 않음. 더 효율적.

인터페이스는 타입에 비해 조금 더 class의 느낌이 많이남.

타입의 장점은 더 많은 형태로 사용되어질 수 있음. 객체의 형태를 명시해주기 위한 인터페이스와 달리 타입으로는 위에서 계속 정리했던 다양한 방식으로 사용가능함.

⇒ 추천 : 객체의 형태를 정해줄 때는 인터페이스 사용. 다른 경우에는 타입 사용

  • 인터페이스방식
interface User {
	name: string
}

interface Player extends User {}

const hanbin : Player = {
	name: 'hanbin'
}

// 인터페이스에서는 property의 축적이 가능하다!
interface User {
	age: number
}

interface User {
	bool: boolean
}

// => User의 프로퍼티를 세번에 걸쳐 정의해줘도 ok
const hanbin: Player = {
	name: 'hanbin',
	age: 1,
	bool: true
}
  • 타입 방식
type User = {
	name: string
}

type Player = User & {}

const hanbin: Player = {
	name: 'hanbin'
}

// 타입에서는 인터페이스처럼 여러번에 걸쳐 프로퍼티를 정의해주는 것이 불가능하다
// error! 위의 type User에 추가해줘야함.
type User = {
	age: number
} // 

Interface (2)

어렵다 ㅎ

인터페이스와 타입은 상속을 받는 방법이 조금 다름.

인터페이스를 사용하면 객체의 형태를 지정해줄 수 있음 = 클래스의 property와 메소드를 특정해줄 수 있음

  • abstract class
// abstract class에서는 구현을 하지 않고 구현할 때 따라야 할 규칙을 정함
// abstract class로는 인스턴스를 생성할 수 없음: new User() ❌
abstract class User {
	constructor (
		protected firstName: string, 
		protected lastName: string
	) {}
	abstract sayHi(name: string) : string
	abstract fullName(): string
}

// 상속
class Player extends User {
	fullName() {
		return `${this.firstName} ${this.lastName}`
	}
	sayHi(name) {
		return `Hello ${name}, My name is ${this.fullName())`
	}
}

자바스크립트로 변환하면 abstract class가 아닌 그냥 일반 class로 변환됨.

abstract class의 목적은 다른 class가 표준화된 property와 method를 가지도록 규칙을 정해주는 것.

따라서 자바스크립트 영역에서는 사실 필요가 없음 ⇒ 인터페이스를 쓰면 해결!

  • interface로 바꿔보자
// JS 변환시 사라짐
interface User {
	firstName: string,
	lastName: string,
	sayHi(name:string): stirng
	fullName(): string
}

// 클래스가 인터페이스를 상속받으려면 implements를 사용함 : 이건 타입스크립트만 가능한 명령어
class Player implements User {
	constructor(
		// ❗️인터페이스를 상속받으면 property를 public으로만 지정할 수 있음
		// error
		private firstName: string,
		protected lastName: string
		// correct
		public firstName: string,
		public lastName: string
	) {}
	fullName() {
		return `${this.firstName} ${this.lastName}`
	}
	sayHi(name) {
		return `Hello ${name}, My name is ${this.fullName())`
	}
}

// 여러개의 인터페이스를 동시에 상속 받을 수도 있음
interface Human {
	health: number
}

class Player2 implements User, Human {....}

인터페이스를 사용할 때의 단점

  1. property를 public으로만 지정 가능
  2. 추상클래스에서 constructor로 만들면 상속받은 클래스에서는 만들어줄 필요가 없는데 인터페이스는 상속받은 클래스에서 매번 지정해줘야함. 귀찮..

❗️인터페이스를 타입으로 지정해줄 수도 있음

0개의 댓글