💡 정리노마드코더의 타입스크립트 강의를 듣고 정리한 내용입니다.
https://nomadcoders.co/typescript-for-beginners/lobby
// 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가 없는 경우에 대한 오류발생가능성 차단
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가 들어오면 에러없이 잘 동작
type Player = {
readonly name: Name,
age?: Numbber
}
// 읽기전용으로 만들고 싶으면 앞에 readonly를 추가!
// 일반 데이터값이든 배열이든 객체든 그 안의 값이 const 처럼 수정할 수 없게됨
튜플은 배열을 생성해줌. 최소length값이 있고 특정 위치에 특정 값이 있어야함
cosnt Player : [string, number, boolean] = ['hanbin', 29, true];
// 반드시 3개의 값을 가져야하며, 각 위치별로 정해진 타입과 일치해야함
// readonly를 붙일 수 있음
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')
}
콜시그니쳐 = 함수에 마우스 올리면 위에 나타나는 설명글. 인자나 리턴값의 데이터타입을 명시해줌
이걸 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로 함수를 보내려고 하면 타입스크립트에게 이 함수가 어떻게 구성된건지 알려줘야함. 그럴때 사용.. 근데 이때 정확히 어떻게 코드로 구현되는지는 좀 봐야 알거같음
함수가 여러개의 다른 콜시그니쳐를 가지고 있을때 발생됨
실제로 오버로딩을 쓰는 코드를 작성할 일은 많지 않음. 근데 외부 라이브러리 같은거는 오버로딩 많이 써서 얘의 개념이랑 어떻게 동작하는지를 알아둬야함.
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
}
// 확실히 라이브러리에서 함수를 가져와 쓸 때 옵셔널하게 들어가는 인자가 꽤 있었고
확실히 라이브러리에서 함수를 가져와 쓸 때 옵셔널하게 들어가는 인자가 꽤 있었던 기억이 있는데 그거의 라이브러리 내부적인 처리과정을 딥하게 생각해본적이 없었다. 이거 알고 생각해보니까 이렇게 처리를 했겠구나싶음.
다양한 타입이 될 수 있는 콜 시그니쳐
함수를 해당 콜시그니쳐로 지정하면 들어오는 인자값에 따라 타입이 정해짐
제네릭은 내가 요구한대로 콜시그니쳐를 만들어 줌
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 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 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', '비니')
인터페이스는 타입이랑 비슷하게 쓰면 되는데 다른건 할 수 없고 객체의 형태
를 지정해주는 용도로만 쓸 수 있음
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
} //
어렵다 ㅎ
인터페이스와 타입은 상속을 받는 방법이 조금 다름.
인터페이스를 사용하면 객체의 형태를 지정해줄 수 있음 = 클래스의 property와 메소드를 특정해줄 수 있음
// 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를 가지도록 규칙을 정해주는 것.
따라서 자바스크립트 영역에서는 사실 필요가 없음 ⇒ 인터페이스를 쓰면 해결!
// 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 {....}
인터페이스를 사용할 때의 단점
❗️인터페이스를 타입으로 지정해줄 수도 있음