본문 바로가기
개발자 도전기/[STUDY] JS || TS

TypeScript | 강의 메모 | 타입 정의

by 답수 2022. 3. 31.
728x90
SMALL

 

 

※ 인프런 - 타입스크립트 시작하기(by 이재승) 강의를 기반으로 정리한 내용입니다.

 

 

타입스크립트 기본 타입

export {};

const size: number = 123;
const isBig: boolean = size >= 100;
const msg: string = isBig ? "크다" : "작다";

// 배열의 타입 선언 방법은 아래와 같이 두 가지 방법이 있음
const values: number[] = [1, 2, 3];
const values2: Array<number> = [1, 2, 3];
// values.push("a");   // <--- error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

// tuple타입: 각 인덱스 별로 타입을 미리 정의
const data: [string, number] = [msg, size];
data[0].substring(1);
// data[1].substring(1);   // <--- error TS2339: Property 'substring' does not exist on type 'number'.

console.log("typeof 123 =>", typeof 123);
console.log("typeof 'abc' =>", typeof 'abc');
console.log("typeof [1,2,3] =>", typeof [1,2,3]);

 

 

export {};

// undefined와 null 역시 타입으로 정의 가능
let v1: undefined = undefined;
let v2: null = null;
// v1 = 123;   // <--- error TS2322: Type '123' is not assignable to type 'undefined'. 

// undefined와 null은 보통 다른 타입과 같이 사용됨
// 기호 " | " 은 유니온 타입
let v3: number | undefined = undefined;
v3 = 123;

console.log("typeof undefined =>", typeof undefined);       // typeof undefinde => undefined
console.log("typeof null =>", typeof null);                 // typeof null => object
/**
 * js에서 undefined는 undefined라는 타입으로 존재하지만, 
 * null은 타입으로 존재하지 않고 object로 표현됨
 */


// TS는 숫자와 문자열의 리터럴도 타입으로 정의 가능
let v4: 10 | 20 | 30;   // v1은 10 or 20 or 30을 가질 수 있는 타입
v4 = 10;
// v4 = 15;    // <--- error TS2322: Type '15' is not assignable to type '10 | 20 | 30'.

let v5: "경찰관" | "소방관";
// v5 = "의사";    // <--- error TS2322: Type '"의사"' is not assignable to type '"경찰관" | "소방관"'.


// any타입을 통해 모든 값을 포함하는 타입 사용 가능
let value: any;
value = 123;
value = "456";
value = () => {};
/**
 * any타입은 기존에 자바스크립트 코드로 작성된 프로젝트를 타입스크립트로 포팅하는 경우 유용하게 사용 가능
 * 기존 프로젝트의 모든 코드에 타입을 한 번에 정의하는 것은 부담되기 때문에 타입 에러가 나는 부분은
 * 임시로 any타입으로 정의하면 됨
 * any타입은 타입을 알 수 없는 경우나 타입 정의가 안 된 외부 패키지를 사용하는 경우에도 사용하기 좋음
 * 단, any타입을 남발하면 TS 사용 의미가 퇴색되기 때문에 되도록 피하는 것이 좋음
 */


// void, never 타입
function f1(): void {
    console.log("hello");
}

function f2(): never {
    throw new Error("some error");
}

function f3(): never {
    while (true) {
        // ..
    }
}
/** 
 * void: 아무 값도 반환하지 않고 종료되는 함수의 반환 타입은 void타입으로 정의할 수 있음
 * never: 항상 예외가 발생해서 비정상적으로 종료되거나 무한루프 때문에 종료되지 않는 함수의 반환 타입은 never타입으로 정의
 * 보통 never는 거의 사용하지 않음
 */


// 객체 타입 object
let v6: object;
v6 = {name: "dapsu"};
// console.log(v6.age);    // <--- error TS2339: Property 'age' does not exist on type 'object'.


// 유니온( | ), 인터섹션( & )
let v7: (1 | 3 | 5) & (3 | 5 | 7);
v7 = 3;
// v7 = 1; // <--- error TS2322: Type '1' is not assignable to type '3 | 5'. 3과 5만 사용 가능하다는 문구 나옴
/**
 * 유니온과 인터섹션을 이용하여 여러 타입의 교집합과 합집합을 표현할 수 있음
 */


// type키워드로 타입 별칭 주기
type width = number | string;
let width: width;
width = 100;
width = "100px";

 

 

enum타입

export {};

// enum 타입
enum Fruit {
    Apple,
    Banana = 5,
    Orange,
}

const v1: Fruit = Fruit.Apple;
const v2: Fruit.Apple | Fruit.Banana = Fruit.Banana;

console.log(Fruit.Apple, Fruit.Banana, Fruit.Orange);   // output: 0 5 6

// 이름과 값이 양방향으로 매핑됨
console.log(Fruit.Banana);      // 5
console.log(Fruit["Banana"]);   // 5
console.log(Fruit[5]);          // Banana
/**
 * enum타입은 js에는 없고 ts에만 있음
 * 보통 자바와 같은 다른 언어에서는 enum이 존재함
 * enum 안에 있는 원소는 타입뿐만 아니라 값으로도 사용 가능. 첫 번째 원소에 값을 할당하지 않으면 자동으로 0이 할당
 * enum의 각 원소에는 숫자 또는 문자열을 할당할 수 있음
 * 명시적으로 값을 입력하지 않으면 이전 원소에서 1만큼 증가한 값이 할당됨
 * enum의 각 원소는 이름과 값이 양방향으로 매핑이 됨

    *** 컴파일했을 때 ***
    var Fruit;
    (function (Fruit) {
        Fruit[Fruit["Apple"] = 0] = "Apple";
        Fruit[Fruit["Banana"] = 5] = "Banana";
        Fruit[Fruit["Orange"] = 6] = "Orange";
    })(Fruit || (Fruit = {}));

 * enum은 객체로 사용되기 때문에 해당 객체를 런타임에 사용할 수도 있음
 */

enum Language {
    Korean = "ko",
    English = "en",
    Japanese = "jp",
}
/**
 * enum 아이템의 값은 숫자뿐만 아니라 문자열로도 입력 가능
 * 문자열은 컴파일했을 때 숫자와 다른 코드가 나옴

    *** 컴파일 ***
    var Language;
    (function (Language) {
        Language["Korean"] = "ko";
        Language["English"] = "en";
        Language["Japanese"] = "jp";
    })(Language || (Language = {}));

 * enum의 원소에 숫자를 할당하면 양방향으로 매핑된 것과 다르게
 * 문자열을 할당하는 경우에는 단방향으로 매핑됨
 */

function getIsValidEnumValue(enumObject: any, value: number | string) {
    return Object.keys(enumObject)                  // enum 객체에서 모든 키를 뽑아냄
        .filter(key => isNaN(Number(key)))          // 양방향 매핑 때문에 filter를 이용하여 키가 숫자인 경우는 삭제
        .some(key => enumObject[key] === value);    // enum 객체 안에 입력된 value가 있는지 검사
}
/**
 * enum을 제대로 이해했으면 위와 같은 유틸리티 함수를 작성할 수 있음
 * 위 함수는 어떤 객체에 특정 value가 있는지 검사하는 함수
 * 
 */

// enum의 아이템의 이름은 false가 나오고 value는 true 나옴
console.log("1 in Fruit: ", getIsValidEnumValue(Fruit, 1));                     // 1 in Fruit:  false
console.log("5 in Fruit: ", getIsValidEnumValue(Fruit, 5));                     // 5 in Fruit:  true
console.log("Orange in Fruit: ", getIsValidEnumValue(Fruit, "Orange"));         // Orange in Fruit:  false
console.log("ko in Language: ", getIsValidEnumValue(Language, "ko"));           // ko in Language:  true
console.log("Korean in Language: ", getIsValidEnumValue(Language, "Korean"));   // Korean in Language:  false


// const enum 사용하기
const enum Books {
    book1,
    book2,
    book3
}
const books: Books = Books.book1;

const enum Menu {
    Menu1 = "samgyeopsal",
    Menu2 = "hangjeongsal",
    Menu3 = "gabrisal"
}
const menu: Menu = Menu.Menu1;
/**
 * enum을 사용하면 컴파일 후에도 enum 객체가 남아있기 때문에 번들 파일의 크기가 불필요하게 커질 수 있음
 * enum객체에 접근하지 않는다면 굳이 컴파일 후에 객체로 남겨 놓을 필요가 없음
 * 이럴때는 const enum을 사용해서 컴파일 결과에 enum의 객체를 남겨 놓지 않을 수 있음
 * 위의 코드 컴파일 결과:
 * const books = 0 /* book1 *\/;
 * const menu = "samgyeopsal" /* Menu1 *\/;
 * enum객체는 없고 사용한 값만 노출됨!
 * const enum을 사용하면 유틸리티 함수 사용할 수 없음. 이럴 때 Ts에서 const enum이라서 사용할 수 없다고 에러 줌
 */

 

 

함수타입1

export {};

function getText(name: string, age: number): string {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? "senior" : "junior";
    return `name: ${nameText}, age: ${ageText}`;
}

// const v1: string = getText("mike", 23);
// const v2: string = getText("mike", "23");   // error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
// const v3: number = getText("mike", 23);     // error TS2322: Type 'string' is not assignable to type 'number'.
/**
 * 위의 함수는 string타입이기 때문에 반환값이 string이어야 함
 */


// 화살표함수로는 이렇게 표현 가능
const getText2: (name: string, age: number) => string = (name, age) => {    // 함수를 구현하는 코드에서는 타입을 정의할 필요 없음
    return "";
}


// 선택 매개변수(optional parameters)
function getText3(name: string, age: number, language?: string): string {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? "senior" : "junior";
    const languageText = language ? language.substring(0, 10) : "";
    return `name: ${nameText}, age: ${ageText}, language: ${languageText}`;
}

// getText3("dapus", 30, "ko");
// getText3("dapus", 30);
// getText3("dapus", 30, 123);     // error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
/**
 * 매개변수 이름 오른쪽에 물음표 기호를 입력하면 선택 매개변수가 됨
 * language는 문자열도 가능하고 undefined일수도 있음
 * 단, 문자열이 아닌 타입은 에러 발생
 * 선택 매개변수는 마지막 순서에만 있어야 함
 * 다만 
    unction getText3(name: string, age: number | undefined, language: string): string { ...
    getText3("dapsu", undefined, "ko");
 * 이렇게 사용이 가능하지만, 사용성과 가독성이 좋지 않음
 */


// 매개변수 디폴트값 설정
function getText4(name: string, age: number = 15, language = "ko"): string {
    return "";
}
console.log(getText4('dapsu'));
/**
 * age 매개변수 같은 경우, 변수에 값을 부여하지 않아도 15라는 디폴트값이 있음
 * language는 string으로 타입을 정의하지 않았지만, 기본값이 "ko"라는 문자열이 있기 때문에 자동으로 string으로 정의
 * 
 */


// 나머지 매개변수(rest parameters)
function getText5(name: string, ...rest: number[]): string {
    return "";
}
// console.log(getText5("dapsu", 1, 2, 3));
// console.log(getText5("dapsu", 1, 2, "3"));  // <---  error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
/**
 * rest parameters의 타입은 항상 배열로 정의해야 함
 * 위에서는 number의 배열로 정의되어 있기 때문에 문자열이 변수로 들어가면 에러남
 */

 

 

함수타입2

export {};

// 타입스크립트에서 this 정의하는 방법
function getParam(this: string, index: number): string {
    const params = this.split(",");
    if (index < 0 || params.length <= index) {
        return "";
    }
    return this.split(",")[index];
}
/**
 * this의 타입은 매개변수 맨 앞에 정의할 수 있음
 * 타입스크립트는 this가 있으면 맨 앞의 매개변수를 this의 타입이라고 인식함
 * 그래서 함수의 매개변수는 두 번째부터 시작함
 */


// 인터페이스를 이용하여 String에 getParam 메소드 추가
// interface String {
//     getParam(this: string, index: number): string;
// }

// String.prototype.getParam = getParam;
// console.log("asdf, 1234, ok".getParam(1));      // 1234
/**
 * 자바스크립트에 내장된 타입에 기능을 주입하고 싶을 때는 프로토타입을 이용해서 주입할 수 있음
 * 그러나 내장 타입에 getParam이라는 속성이 없기 때문에 interface를 이용하여 속성 추가 가능
 */


// // Object에 메소드 추가하기
// interface Object {
//     getShortKeys(this: object): string[];
// }

// Object.prototype.getShortKeys = function () {
//     return Object.keys(this).filter(key => key.length <= 3);
// };

// const obj = {
//     a: 1,
//     bb: 2,
//     ccc: 3,
//     dddd: 4
// };
// console.log(obj.getShortKeys());        // [ 'a', 'bb', 'ccc' ]


// add함수
/** 조건
 * 두 매개변수가 모두 문자열이면 문자열 반환
 * 두 매개변수가 모두 숫자이면 숫자 반환
 * 두 매개변수를 서로 다른 타입으로 입력하면 안 된다.
 */
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: number | string, y: number | string): number | string {
    if (typeof x === 'number' && typeof y === 'number') return x + y;
    else {
        const result = Number(x) + Number(y);
        return result.toString();
    }
}

// const v1:number = add(1, 2);    // 매개변수 둘 다 숫자지만, v1이 number타입만 정의되어서 문제
// console.log(add(1, '2'));       // 매개변수가 서로 다른 타입이지만 에러가 발생하지 않음
/**
 * add함수 위에 코드 두 줄 넣으면 됨!
    function add(x: number, y: number): number;
    function add(x: string, y: string): string;
 */


// named parameters 방식
// 매개변수 타입 정의를 객체로 정의
function getText({
    name,
    age = 15,
    language
}: {
    name: string;
    age?: number;
    language?: string;
}): string {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? 'senior' : 'junior';
    return `name: ${nameText}, age: ${ageText}, language: ${language}`;
}
getText({name: 'nike'})
getText({name: 'dapsu', age: 30, language: 'ko'})


// 인터페이스로 타입 정의를 따로 만들수도 있음
interface Param {
    name: string;
    age?: number;
    language?: string;
}

function getText2({name, age = 15, language}: Param): string {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? 'senior' : 'junior';
    return `name: ${nameText}, age: ${ageText}, language: ${language}`;
}
/**
 * 매개변수가 많아지면 가독성이 떨어지기 때문에 named parameters로 변경하는 것이 좋음
 * 하지만 위의 방법을 수동으로 하면 다소 번거로움
 * 타입스크립트에서 자체적으로 변환해주는 기능이 있음
 */

// function에 커서를 대면 좌측 상단에 전구 모양 표시됨. 클릭하면 Convert paramteters to destructed object 뜸!
 function getText3(name: string, age: number = 15, language?: string): string {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? 'senior' : 'junior';
    return `name: ${nameText}, age: ${ageText}, language: ${language}`;
}

 

 

인터페이스

/**
 * 자바와 같은 다른 언어에서 인터페이스는 클래스를 구현하기 전에 필요한 메소드를 정의하는 용도로 쓰임
 * 타입스크립트에서는 더 다양한 것들을 정의하는데 사용됨
 * 타입스크립트에서 인터페이스로 정의할 수 있는 타입의 종류와 인터페이스로 타입을 정의하는 방법에 대해서 알아보자.
 */

export {};

// 객체에 타입을 정의하는 방법
/**
 * 인터페이스 키워드 오른쪽에 타입의 이름을 적는다.
 * 괄호 안에 필요한 속성을 입력한다.
 * 선택속성은 속성 이름 오른쪽에 ? 기호를 붙인다.
 * readonly속성은 읽기 전용이기 때문에 변경 불가능
 */
interface Person {
    name: string;
    readonly nickname: string;
    age?: number;
    // age: number | undefined;     // 이 방식은 선택속성 아님. age 항상 입력해야 함
}
const p1: Person = {name: 'leo', nickname: 'dapsu', age: 30};
// p1.nickname = 'handsom guy';    // 읽기 전용 속성으로 에러 발생

// 객체가 정의되지 않은 속성값을 가지고 있어도 할당 가능
const p2 = {
    name: 'mike',
    nickname: 'nike',
    birthday: '2022-02-22'
}
const p3: Person = p2;  // 가능한 이유? p3타입이 p2타입을 포함하는 더 큰 타입이기 때문(타입 호환성 부분에서 자세히 다룰 예정)

// 인덱스 타입: 인터페이스에서 속성 이름을 구체적으로 정의하지 않고 값의 타입만 정의하는 것
interface Person2 {
    name: string;
    age: number;
    [key: string]: string | number; // 인덱스 타입
}
const p4: Person2 = {
    name: 'loo',
    birthday: "2022-02-22",     // 인덱스 타입에 의해 가능
    age: 25,
};


// 자바스크립트는 속성 이름이 숫자면 내부적으로 문자열로 변환해서 사용함
// 타입스크립트에서는 숫자인 속성 이름의 값이 문자열인 속성 이름의 값으로 할당 가능한지 검사함
interface YearPriceMap {
    [year: number]: number;             // 이 숫자의 값 A는 B로 할당이 가능해야 한다.
    [year: string]: string | number;    // 이게 가능하기 위해서는 문자열과 숫자 둘다 정의되어야 함
}
const yearMap: YearPriceMap = {};
// yearMap[1998] = 1000;
// yearMap[1998] = '1000';     // 에러
// yearMap['2000'] = 1234;
// yearMap['2000'] = '1234';   // 이것도 에러. 왜? 타입스크립트 4.4부터 스펙 변경. 인덱스로 문자열을 입력해도 숫자로 파싱 가능하면 숫자로 인식


// 인터페이스로 함수 타입 정의
interface getText {
    (name: string, age: number): string;
}
// 위의 인터페이스와 같은 것임: type GetText = (name: string, age: number) => string;
const getText: getText = function(name, age) {
    const nameText = name.substring(0, 10);
    const ageText = age >= 35 ? 'senior' : 'junior';
    return `name: ${nameText}, age: ${ageText}`;
}


// 자바스크립트에서는 함수도 속성값을 가질 수 있음
interface getText2 {
    (name: string, age: number): string;
    totalCall?: number;     // 인터페이스를 정의할 때 함수의 속성값도 이렇게 정의할 수 있음
}
const getText2: getText2 = function(name, age) {
    if (getText2.totalCall !== undefined) {
        getText2.totalCall++;
        console.log(`totalCall: ${getText2.totalCall}`);
    }
    return "";
}
// getText2.totalCall = 0;
// getText2('', 0);
// getText2('', 0);


// 인터페이스 class로 구현
interface Person3 {
    name: string;
    age: number;
    isYoungerThan(age: number): boolean;
}

class SomePerson implements Person3 {
    name: string;
    age: number;
    constructor(name:string, age: number) {
        this.name = name;
        this.age = age;
    }
    isYoungerThan(age: number) {
        return this.age < age;
    }
}


// 인터페이스 확장
interface Person4 {
    name: string;
    age: number;
}
interface Korean extends Person4 {
    isLiveInSeoul: boolean;
}
/**
 * Korean에는 Person4의 속성들과 isLiveSeoul의 속성이 들어있음
 interface Korean extends Person4 {
     name: string;
     age: number;
     isLiveInSeoul: boolean;
 }
 */

 // 인터페이스 여러 개로 확장도 가능
 interface Programmer {
     language: string;
 }

 interface Korean2 extends Person4, Programmer {
     city: string;
 }


 // &으로 속성값의 교집합
 interface Product {
     name: string;
     price: number;
 }
 type PP = Person4 & Product;
 const pp: PP = {
     name: 'a',
     age: 23,
     price: 1000
 };
 // 교집합이 속성에 대한 교집합이 아니라, 타입이 가질 수 있는 값의 집합에 대한 교집합이기 때문에 가능(자세한 것은 타입 호환성 부분에서!)

 

 

클래스

export {};

class Person {
    name: string;
    // private name: string;
    // #name: string;  // 속성 이름 앞에 # 붙이면 private으로 정의한다는 것과 동일함
    constructor(name: string) {
        this.name = name;
        // this.#name = name;
    }
    sayHello() {
        console.log(`Hola!! Me yamo ${this.name}`);
        // console.log(`Hola!! Me yamo ${this.#name}`);
    }
}

class Programmer extends Person {
    constructor(name: string) {
        super(name);    // 자식 클래스의 컨스트럭터에서는 반드시 super를 호출해야 함(부모 클래스의 컨스트럭터가 호출됨  )
    }
    // method override: 부모의 sayHello가 아닌 자식의 함수가 호출됨
    sayHello() {
        // this.name;      // 만약 부모 클래스의 name이 private이면 에러 발생
        // this.#name;      // 만약 부모 클래스의 name이 private이면 에러 발생
        super.sayHello();   // 부모의 함수를 호출하기 위해서는 super.sayHello()를 호출해야 함
        console.log('난 프로그래머다');
    }
}

const programmer = new Programmer('dapsu');
programmer.sayHello();
// Hola!! Me yamo dapus
// 난 프로그래머다

const person = new Person('홍길동');
console.log(person.name);   // private일 때 에러남


// 접근범위 설정 키워드: public, protected, private
/**
 * public: 외부에도 노출하면서 상속받는 쪽에도 노출을 하는 것
 * private: 외부에도 노출하지 않고 상속받는 쪽에도 노출하지 않는 것 
 * protected: 외부에는 노출하지 않지만 상속받는 쪽에만 노출
 * 메소드를 정의할 때 따로 설정하지 않으면 기본값으로 public이 사용됨
 */


// 타입스크립트 접근범위 편의 기능
class Person2 {
    // 변수 선언, 초기화하는 코드도 필요 없음. 자동으로 해줌
    constructor(public name: string, public age: number) {}
}

const person2 = new Person2('dapsu', 30);
console.log(person2.name, person2.age);     // dapsu 30


// getter, setter
class Person3 {
    private _name: string = '';

    // getter 정의
    get name(): string {
        console.log('getter called');
        return this._name;
    }

    // setter 정의
    set name(newName: string) {
        if (newName.length > 10) {
            throw new Error('최대 길이를 넘었습니다');
        }
        this._name = newName;
    }
}

let person3 = new Person3();
person3.name = "dapsu";         // getter called
console.log(person3.name);      // dapsu
// person3.name = "우주제일초절정킹갓미소년";      //   Error: 최대 길이를 넘었습니다


// static키워드 이용하여 정적 멤버 변수와 정적 메소드 정의
class Person4 {
    static adultAge = 20;
    constructor(public name: string, public age: number) {}
    sayHello() {
        console.log(`안녕하세요 저는 ${this.name}입니다`);
        console.log(
            Person4.getIsAdult(this.age) ? '삐빅! 성인입니다' : "삐빅! 청소년입니다"
        );
    }
    static getIsAdult(age: number) {
        return Person4.adultAge <= age;     // adultAge 정적변수이기 때문에 접근 가능
    }
}
/**
 * static 키워드가 붙은 속성은 객체와 상관없이 고정값
 * 그래서 사용할 때 클래스이름에 . 찍어서 접근할 수 있음
 */

const person4 = new Person4('killdong', 24);
person4.sayHello();
// 안녕하세요 저는 killdong입니다
// 삐빅! 성인입니다
console.log(`성인 판단 기준 나이: ${Person4.adultAge}`);    // 성인 판단 기준 나이: 20


// abstract키워드로 추상클래스 정의
abstract class Person5 {
    constructor(public name: string) {}
    sayHello() {
        console.log(`Hey, I'm ${this.name}`);
    }
    abstract sayHello2(): void;
}

class Student extends Person5 {
    sayHello() {
         super.sayHello();
         console.log("I'm a student");
    }
    // 이 함수 상속받지 않으면 에러뜸
    sayHello2(): void {
        console.log("Wassssssssssssup!!!!!!!!!");
    }
}

// const person5 = new Person5('Tom');     // 추상클래스는 객체로 만들 수 없음

 

 

 

 

 

 

 

 

728x90
LIST

댓글