프론트엔드

[TS] 이것만 알아도 기초탄탄? TypeScript 기초 정리!

CodeBoyEd 2021. 12. 16. 16:27

! 이 글은 제가 너무 애청하는 유투브 "코딩앙마" 채널의 TypeScript 강좌를 정리한 글입니다.

코딩앙마님 유투브 강의 바로가기


Type Script 기초

유투브 코딩앙마님 강의 정리 by 웅


[1, 2강] 타입스크립트의 기본 타입

let age:number = 30;
let isAdult:boolean = true;
let a:number[] = [1,2,3];
let b:Array<number> = [1,2,3];

let week1:string[] = ['mon', 'tue', 'wed'];
let week2:Array<string> = ['mon', 'tue', 'wed'];

week1.push('ja')
  • 리스트 타입은 위와 같이 string[ ] , Array 이렇게 두 가지 정의 방법이 있다.

 

튜플 : 리스트인데, 자리마다 들어가는 type이 달라지는 경우

let c:[string, number];

c = ['z', 11111];
let d:string = c[1].toLocaleString();

 

함수 타입 중 void 와 never

  • void : 반환하는 값이 없는 함수에게 정의
  • never : 에러를 반환하거나 무한 반복 같이 영원히 끝나지 않는 함수의 경우
function sample():void {
    console.log('hi');
}

function sample2():never {
    while(true) {
        console.log('hi')
    }
}

function sample3():never {
    throw new Error('error');
}

 

enum : 원래 JS 에는 없다! 비슷한 type끼리 묶어준 객체, 수동으로 값을 주지 않으면 0부터 1씩 증가하면서 할당됨


enum Os {
    window,
    ios,
    android
}

//수동으로 정의하면 해당 값을 가짐, 아래의 경우 android = 11이 들어갈거임
enum Os {
    window=3,
    ios=10,
    android
}

//숫자가 매핑되어 있다는 것은 양방향이 가능하다는 것! 숫자만!
console.log(Os['android']);
console.log(Os[11]);

//문자는 양방향이 불가능 console.log(Os2['win'])하면 에러남!
enum Os2 {
    Window = 'win',
    Ios = 'ios', 
    Android = 'android',
}

//특정 값만 입력 가능
let myOs:Os2;

myOs = Os2.Window;
  • enum이 숫자인 경우에는 양방향 호출이 가능하지만, 문자인 경우에는 불가
  • enum은 주로 특정 값만 입력하도록 제약하고 싶고 그 특정값들이 공통점을 가질때 사용함

 

null, undefined 타입

//null, undefined도 타입이 있음
let e:null = null;
let f:undefined = undefined

[3강] 인터페이스

let user:object = {}
//이렇게 선언도 하지만 object 에는 property를 정의할 수가 없음
//이럴 때는 인터페이스를 선언!

type Score = 'A' | 'B' | 'C' | 'F';

interface User {
    name: string,
    age: number,
    gender?: string;
    readonly birthYear: number;
    [grade: number]: Score;
        //이 경우는 number 를 키로 하고 score를 발류로 하는 값이 여러개 가능하다는 뜻
}

let user:User = {
    name: 'xx',
    age: 12,
    birthYear: 2000,
    1: 'A',
    2: 'B',
    3: 'F',
}
  • [grade: number]: Score 는 number 타입을 key로 하고 Score 값 중 하나를 value로 하는 데이터가 존재할 수도 있고 아닐 수도 있음을 의미

함수 정의도 가능 ( 변수로 함수 정의하는 경우에만 사용 가능 )

interface Add {
        // 인자 값 : 리턴 값
    (num1: number, num2: number): number;
}

const add: Add = (x, y) => {
    return x+y;
}

interface isAdult {
    (age: number): boolean;
}

const isAdult:isAdult = (age) => {
    return age>19;
}

 

자바처럼 인터페이스로 Class도 정의 가능 ( Implement라는 키워드 사용 )

interface Car {
    color: string;
    wheels: number;
    start(): void;
}

interface Benz extends Car {
    door: number;
    stop(): void;
}

class Bmw implements Car {
    color;
    wheels = 4;
    constructor(color: string) {
        this.color = color;
    }
    start() {
        console.log('start');
    }
}

class myBenz implements Benz {
    door = 4;
    color;
    wheels = 4;
    constructor(color: string) {
        this.color = color;
    }

    start() {
        console.log('start');
    }

    stop () {
        console.log('benz go...');
    }

}

const myCar = new Bmw('green');
console.log(myCar.color);
myCar.start();

 

Implement 의 사용법

interface Car {
    color: string,
    wheels: number;
    start(): void;
}

interface Brand {
    logo: string,
    marketing(): void;
}

//이렇게 두 개 상속도 가능
interface Benz extends Car, Brand{
    name?: string;
}

const myBenz:Benz = {
    color: 'red',
    wheels: 4,
    start: () => {
        console.log('start');
    },
    logo: 'benz c class',
    marketing: () => {
        console.log("i'm benz");
    },
}

class MyBenz implements Benz {
    color;
    wheels;
    logo;

    constructor(color: string, wheels:number, logo:string) {
        this.color = color;
        this.wheels = wheels;
        this.logo = logo;
    }

    start () {
        console.log('start');
    }

    marketing () {
        console.log('marketing');
    }
}

[4강] 함수

function hello(name?: string) {
    return `Hello, ${name || "world"}`;
}

function hello2(name = "world") {
    return `Hello, ${name}`;
}
  • 이렇게 두 개는 같은 의미를 갖는다. name이 string 으로 들어오거나, undefined 이거나

주의해야할 점

 function hello(name?: string, age: number) {
    return `Hello, ${name}, ${age} `;
}
  • 위 사례처럼 Optional 한 값을 앞에 두면 안됨!

나머지 매개변수

function add(...nums: number[]) {
    return nums.reduce((prev, curr) => prev + curr, 0);
}

add(1,2,3);
add(1,2,3,4,5,5,6,7,7,8,);

 

함수에서 this

일단 Arrow Function 은 this 라는 값을 못 가진 단다...

interface User {
    name: string;
}

const Sam:User = {
    name:'Sam'
}

function showName (this:User, age: number, gender: 'm'|'w') {
    console.log(this.name, age, gender);
}

const a = showName.bind(Sam);
a(30, 'm');
  • this는 매개변수 가장 앞에 선언하고, this 로 선언될 녀석의 타입을 정의해주면 된다.

오버로드

  • 함수에 들어오는 매개변수의 Type 에 따라 반환하는 값이 달라질 때, 사용되는 문법
interface User {
    name: string;
    age: number;
}

function join (name: string, age: string): string;
function join (name: string, age: number): User;
function join (name: string, age: number | string): User | string {
    if (typeof age === "number") {
        return {
            name,
            age,
        };
    } else {
        return "나이는 숫자로 입력해주세요.";
    }
}

const sam: User = join("Sam", 30);
const jane: string = join("Jane", "30");
  • 역시나 화살표 함수로 하니까 const 라서 안된다...

[5강] 리터럴, 유니온/교차 타입

const userName = "Sam";
let userName2 = "Tom";

type Job = "police" | "developer" | "teacher"

interface User {
    name : string;
    job: Job;
}

const user: User = {
    name : "Bob",
    job: "police",
}

interface HighSchoolStudent {
    name: number | string;
    grade: 1 | 2 | 3;
}
  • let은 선언된 녀석의 type을 type으로 지정하지만, const 는 선언된 녀석 자체를 type으로 지정한다. ex) 위 예제에서 userName 의 type은 "Sam", userName2의 type은 "string"
  • type으로 내가 원하는 요소들만 타입으로 지정할 수 있다.
  • number | string 에서 | 는 union이라고 하는데 union type에 대해 더 알아보자

Union 타입

interface Car {
    name: "car";
    color: string;
    start(): void;
}

interface Mobile {
    name: "mobile";
    color: string;
    call(): void;
}

function getGift(gift: Car | Mobile) {
    gift.start(); //에러발생
}

이렇게 하면 gift가 Mobile 일 경우 start() 라는 메소드가 없기 때문에 에러가 발생한다.

interface Car {
    name: "car";
    color: string;
    start(): void;
}

interface Mobile {
    name: "mobile";
    color: string;
    call(): void;
}

function getGift(gift: Car | Mobile) {
    if(gift.name === "car") {
        gift.start();
    } else {
        gift.call();
    }
}
  • 그래서 이렇게 식별할 수 있는 값을 기준으로 타입을 나눠서 각자에 맞는 메소드를 적용한다.
  • 만약 type이 많아진다면 switch 를 사용하는 것이 좋음. 이렇게 구별이 가능한 type을 식별가능한 union type이라고 한다.

Intersection Types ( 교차 타입 )

interface Car {
    name: string;
    start(): void;
}

interface Toy {
    name: string;
    color: string;
    price: number;
}

// 교차 타입
const toyCar: Toy & Car = {
    name: "타요",
    start() {},
    color: "blue",
    price: 1000,
}
  • 교차 타입은 교차되는 인터페이스들의 모든 변수 & 메소드를 정의 해줘야 함

[6강] 클래스

class Car {
    color: string; // <= 얘가 없으면 오류 난다. TS 에서는 변수를 미리 정의해줘야함
    constructor(color: string) {
        this.color = color
    }

    start() {
        console.log("start");
    }
}

const bmw = new Car("red");
class Car {
    constructor(public color: string) {
        this.color = color
    }
}

or 

class Car {
    constructor(readonly color: string) {
        this.color = color
    }
}
  • public 이나 readonly 와 같은 접근 제한자를 이용하면 에러가 사라진다. 이를 알아보자



Access modifier - 접근제한자 ( JS에는 없지만, TS에는 있음 )

  • public ( default ) - 해당 클래스 + 자식 클래스 + 클래스 인스턴스에서도 접근 가능
  • private ( # ) - 해당 클래스 내부에서만 사용 가능
  • protected - 해당 클래스랑 + 자식 클래스에서만 사용 가능
  • readonly - 읽을 수만 있고 수정은 불가능함! 바꾸려면 constructor 에서 this.name = name 으로 바꿔야 함
  • static - 정적 멤버 변수나 메소드 앞에 붙여줌. 접근하기 위해서는 class명.변수(or 메소드)이름 이렇게 접근해야 함

Abstract Class 추상 클래스

abstract class Car {
    color: string;
    constructor(color: string) {
        this.color = color;
    }
    start() {
        console.log("start");
    }
    abstract doSomething(): void;
}

const car = new Car("red"); // 에러 발생
  • 추상 클래스는 new 로 인스턴스를 만들 수 없음
  • 다른 Class 에 상속해서 써야만 함
  • 상속할 때, abstract 가 붙어있는 함수나 변수는 무조건 만들어줘야 함
  • 즉, abstract 키워드를 통해 내부 기능은 달라도 같은 골격? 의 클래스를 강요할 수 있음
abstract class Car {
    color: string;
    constructor(color: string) {
        this.color = color;
    }
    start() {
        console.log("start");
    }
    abstract doSomething(): void;
}

class Bmw extends Car {
    constructor(color: string) {
        super(color);
    }
    doSomething() {
        console.log(3);
    }
}

const z4 = new Bmw("red");

[7강] 제네릭 ( Generic )

function getSize(arr: number[] | string[] | boolean[] | object[]): number {
    return arr.length;
}

const arr1= [1,2,3];
getSize(arr1);

const arr2 = ['1', '2', '3'];
getSize(arr2);

const arr3 = [true, false, true]
getSize(arr3);

const arr4 = [{}, {name: 'woong'}];
getSize(arr4);
  • 위 상황을 보자, 매개변수에 들어올 수 있는 값이 다 제각각 이기 때문에 이를 모두 union으로 선언한 상황이다 (혹은 함수 오버로드 사용) 이럴 경우, 종류가 계속 많아지면 그걸 다 입력할 수 없으니... 함수를 사용할 때, 사용할 타입을 알려줄 수 있다면 얼마나 좋을까? 대신 리스트 라는 것은 알려줄테니! 약간 이런 생각을 하게 된다. 그게 바로 제네릭이다!
//<여기 오는 애>를 타입 파라미터라고 하는데 보통 T라고 쓴다. 다른애들을 써도 상관없다. 
function getSize<T>(arr: T[]): number {
    return arr.length;
}

const arr1= [1,2,3];
getSize<number>(arr1);

const arr2 = ['1', '2', '3'];
getSize<string>(arr2);

const arr3 = [true, false, true]
getSize(arr3);

const arr4 = [{}, {name: 'woong'}];
getSize(arr4);
  • 아래 두 개처럼, 사실 제네릭을 사용할 때 명시해주지 않아도 TS가 알아서 가장 좁은 범위로 Type을 설정한다. 뭐, 그래도 가능하면 명시해 주는 것이, 내 생각대로 코딩을 하게 해서, 버그를 최소화할 수 있는 방법이겠지?

예제

// option에 뭐가 올 지 모르는 경우!
interface Mobile<T> {
    name: string;
    price: number;
    option: T;
}

const m1: Mobile<{ color: string; coupon: boolean }> = {
    name: "s21",
    price: 1000,
    option: {
        color: "red",
        coupon: false,
    }
}

const m2: Mobile<string> = {
    name: "s20",
    price: 900,
    option: "good",
}

제네릭을 이용하여 매개변수가 객체인 경우, 객체에 꼭 들어가야 하는 값들을 특정할 수도 있다.

interface User {
    name: string;
    age: number;
}

interface Car {
    name: boolean;
    color: string;
}

interface Book {
    price: number;
}

const user: User= { name: "a", age: 10 };
const car: Car = { name: true, color: "red" };
const book: Book = { price: 3000 };

function showName<T extends {name: string}>(data: T): string {
    return data.name
}

showName(user);
showName(car); //에러 발생 name: boolean 이기 때문에
showName(book); //에러 발생 name이 없기 때문에

[8강] 유틸리티 타입

keyof

interface User {
    id: number;
    name: string;
    age: number;
    gender: "m" | "f";
}

type UserKey = keyof User; // 'id' | 'name' | 'age' | 'gender'

const uk:UserKey = "age";
  • keyof 를 사용하면 인터페이스에 정의 된 타입들을 type으로 가져올 수 있다.

Partial ( 프로퍼티를 부분적으로 정의할 수 있도록 해줌, 일일이 '?' 를 안달아도 되겠지!? )

interface User {
    id: number;
    name: string;
    age: number;
    gender: "m" | "f";
}

let admin: Partial<User> = {
    id: 1,
    name: "Bob",
}

 

Required 는 반대로 '?'로 준 프로퍼티가 있어도 모두 필수로 바꿔준다.

interface User {
    id: number;
    name: string;
    age?: number;
}

let admin: Required<User> = {
    id: 1,
    name: "Bob",
    age: 20 // age? 이지만, Required 때문에 age를 안적으면 에러가 남
}

 

Readonly : 같은 타입이라도 수정이 불가능!

interface User {
    id: number;
    name: string;
    age?: number;
}

let admin: Readonly<User> = {
    id: 1,
    name: "Bob",
    age: 20
}

admin.id = 4; //에러 발생

 

Record<K, T> : K는 Key, T는 Type

interface Score {
    "1": "A" | "B" | "C" | "D";
    "2": "A" | "B" | "C" | "D";
    "3": "A" | "B" | "C" | "D";
    "4": "A" | "B" | "C" | "D";
}

const score:Score = {
    1: 'A',
    2: 'B',
    3: 'C',
    4: 'D',
}
  • 이렇게 타이핑하기 번거로운 코드를 아래와 같이 바꿔준다.
type Grade = '1' | '2' | '3' | '4';
type Score = 'A' | 'B' | 'C' | 'D';

const score: Record<Grade, Score> = {
    1: 'A',
    2: 'B',
    3: 'C',
    4: 'D'
};
  • 훨씬 깔끔해졌다!

레코드 예제 하나 더!

interface User {
    id: number;
    name: string;
    age: number;
}

function isValid(user: User):Record<keyof User, boolean> {
    const result: Record<keyof User, boolean> = {
        id: user.id > 0,
        name: user.name !== "",
        age: user.age > 0,
    }
    return result;
}

 

Pick<T,K> : Interface 에서 Key로 명시한 애들만 뽑아와서 쓸 수 있음

interface User {
    id: number;
    name: string;
    age: number;
    gender: "M" | "W";
}

const admin: Pick<User, "id" | "name"> = {
    id: 0,
    name: "Bob",
}

 

Omit<T, K> : Pick이랑 반대로 Key로 명시한 애들만 제외하고 쓸 수 있음

interface User {
    id: number;
    name: string;
    age: number;
    gender: "M" | "W";
}

const admin: Omit<User, "age" | "gender"> = {
    id: 0,
    name: "Bob",
}

 

Exclude<T1, T2> : Omit과 비슷한데, Omit은 인터페이스에서 K에 해당하는 프로퍼티를 제거하는 거였다면, Exclude는 T1 타입에서 T2 타입과 겹치는 애들을 제거하는 것!

type T1 = string | number | boolean;
type T2 = Exclude<T1, number | string>;
// T2에는 number 와 string 이 제거 되고 boolean 만 남게 됨

 

NonNullable : null 과 undefined 가 있으면 걔네를 제외한 타입을 생성한다.

type T1 = string | null | undefined | void;
type T2 = NonNullable<T1>;
// T2에는 string 과 void만 남음