TypeScript 무엇인가? 직접알아보자! - 기본문법 정리(1)

신은수·2023년 11월 13일
0

TypeScript

목록 보기
1/2

1. 메인룰

  • typescript는 최종적으로 javascript로 변환된다. 순전한 typescript 코드를 돌릴 수 있는 것은 deno이나 대중화되지가 않았음. 브라우저, 노드는 모두 js 파일을 실행한다.
  • typescript는 언어이자 컴파일러(tsc)이다. 컴파일러는 ts 코드를 js로 바꿔준다.

💡 tsc는 1. 코드 변환하는 역할 2. 타입검사하는 역할
1과 2가 별개여서 타입검사에서 에러가 나도 코드를 js로 변환할 수 있다


2. ts 프로젝트 만들기

  • npx tsc --init: tsconfig.json 생성

  • npx tsc : ts파일을 js로 바꾸는 명령어

  • npx tsc --noEmit : 타입검사만 하는 명령어. 에디터를 사용하지 않으면 해당 명령어를 계속해줘야함

  • "allowJs": true : ts와 js를 동시에 쓸 수 있음

  • esModuleInterop: true, strict: true : 필수

  • "skipLibCheck": true : 라이브러리는 타입검사하지 말라

  • "forceConsistentCasingInFileNames": true : 윈도우에서는 대소문자 지키지 않고 import해도 되는데 리눅스에서는 안된다. 따라서 서버에 올렸을때 문제 될 수 있으므로 윈도우에서도 대소문자 지켜야 되도록 하는 옵션이다


3. 기본타입

문자열 타입, 숫자타입, 진위(boolean) 타입, , 객체타입, 배열타입, 튜플타입, null, undefined, never, any, unknown, void

1) object타입

  • 타입스크립트의 장점을 극대화하려면 가급적 타입을 최대한 구체적으로 선언해야한다.
  • 이 관점에서 볼 때 object타입은 어떤 속성이 있고 무슨 타입을 갖는지 명시되어 있지 않으므로 자바스크립트를 사용하는 것과 크게 차이가 없다.
  • 따라서 객체를 타이핑할 때 object는 지양해야한다. (interface나 type, class 등을 사용하자)

2) 튜플타입

  • 튜플은 특정 형태를 갖는 배열을 의미한다. 배열 길이가 고정되고 각 요소 타입이 정의된 배열을 튜플이라 한다.
    const tuple: [string, number] = ["1", 1];
    tuple[2] = "hello"; // 에러
    tuple.push("hello"); // 이건 잘 됨

3) never 타입

  • 타입스크립트에서 never 타입은 값의 공집합이다.
  • never 타입이 왜 필요할까?: 숫자 체계에 아무것도 없는 양을 나타내는 0처럼 문자 체계에도 불가능을 나타내는 타입이 필요하다.
  • const array = [] : 빈배열 선언하면 array를 never[]로 추론
  • never타입 완벽 가이드

4) ⭐ any타입과 unknown 타입

  • any와 unknown 둘다 없으면 좋지만, any보다는 unknown이 낫다.
  • any는 타입검사를 아예 포기해버리지만, unknown은 타입검사를 포기하지 않는다. (unknown은 나중에라도 타입을 지정할 수 있다. )
    interface A {
    	talk: ()=> number;
    }
    const a: A = {
    	talk: ()=> {
    		return 3
    	}
    }
    
    // any는 타입검사를 포기해버림. 
    const b: any = a.talk();
    b.play(); // 타입에러는 안나지만 b에 play속성이 없으므로 런타임에서 에러난다.
    
    // unknown은 타입검사를 포기하지 않는다.
    const c: unknown = a.talk();
    c.talk(); // 'c is typeof unknown' 에러가 난다.
    (c as A).talk() // unknown은 나중에라도 타입을 지정할 수 있다. 
  • unknown이 나올 수 있는 예시

6) ⭐ void

a. return 값 있으면 안 된다.
b. return 값이 있어도 되나, return 값이 뭐든간에 return 값을 사용하지 않겠다.

  • return값이 void인 함수-> return값 있으면 안 됨

  • return값이 void인 method -> return 값 있어도 됨 = return 값을 사용하지 않겠다.

  • return값이 void인 매개변수 함수 -> return 값 있어도 됨 = return 값을 사용하지 않겠다.

    // push는 return값이 number이다. 
    function forEach(arr: number[], callback: (el: number) => undefined): void;
    const target: number[] = [];
    forEach([1, 2, 3], (el) => target.push(el)); // 에러난다
    
    function forEach(arr: number[], callback: (el: number) => void): void;
    const target: number[] = [];
    forEach([1, 2, 3], (el) => target.push(el)); // 에러안난다

  • undefined는 void에 대입 가능하다. void는 undefined에 대입이 불가능했는데 ts5.1부터는 가능하게 되었다

    // undefined는 void에 대입 가능하다.
    function a():void{
     return undefined;
    }
    
    // null은 void에 대입 불가능하다.
    function a():void{
     return null;
    }
    
    // void는 undefined에 대입이 불가능하다. ts 5.1부터는 가능하게 되었다.
    function a():undefined{
    }

7) ⭐ {}와 Object

const x: {} = "hello"; // 에러가 나지 않는다.
const y: Object = "hi"; // 에러가 나지 않는다.
const xx: object = "hi"; // 에러난다.
const yy: object = { hello: "world" }; 
const z: unknown = "hi";

// * unknown = {} | null | undefined
if (z) {
  z; // {}로 추론된다. 왜냐면,
} else {
  z; // null과 undefined는 else로 가기 때문에
}
  • {}(=Object) 타입은 null과 undefined를 제외한 모든 타입을 의미한다.


4. 인터페이스

1) 인터페이스 상속

  • 인터페이스를 상속받을때 클래스와 동일하게 extends라는 예약어를 사용한다.
  • 상속을 여러번 할 수 있다.
interface IAnimal {
  breath: true;
}

interface IHuman extends IAnimal {
  think: true;
}

// 상속을 여러번 할 수 있다.
interface IHuman {
  talk: true;
}
const ieunsu: IHuman = { breath: true, think: true, talk: true };

2) 인덱스 시그니처

  • 정확히 속성이름을 명시하지 않고, 속성 일므의 타입과 속성 값의 타입을 정의하는 문법 (객체와 배열을 인덱싱할 때 활용된다)
interface A {
  [key: string]: string;
}
const a: A = { a: "hello", b: "world" };

// 문자열 배열은 string[]으로 선언하는 것이 더 간편하고 타입을 파악하기 편함
interface B {
  [key: number]: string;
}
const a: B = ["hello", "world"];


5. 타입별칭(Type Alias)

1) 타입 별칭 타입 확장

  • 인터페이스는 타입을 확장할 때 상속이라는 개념을 이용
  • 인터섹션 타입(&)으로 타입을 확장함
    type Person = {
      name: string;
      age: number;
    };
    type Developer = {
      skill: string;
    };
    var joo: Person & Developer = {
      name: "은수",
      age: 25,
      skill: "프론트엔드 개발",
    };

2) 타입 별칭과 인터페이스의 차이

  • 코드에디터에서 표기 방식 차이
  • 사용할 수 있는 타입의 차이
    • 인터페이스는 주로 객체의 타입을 정의하는데 사용하는 반면,
    • 타입 별칭은 일반 타입에 이름을 짓는데 사용하거나 유니언 타입, 인터섹션 타입 등에도 사용할 수 있음, 또한 타입 별칭은 제네릭이나 유틸리티 타입 등 다양한 타입에서 사용할 수 있음
  • 타입 확장 관점에서의 차이
    • 인터페이스는 타입을 확장할 때 상속(extends)이라는 개념을 이용
    • 인터섹션 타입(&)으로 타입을 확장함


5. enum

1) enum이란 (숫자형, 문자형)

  • 특정 값의 집합을 의미하는 데이터 타입
  • 숫자형 enum
    enum EDirection {
      Up = 3,
      Down,
      Left,
      Right,
    }
  • 문자형 enum
    enum EDirection {
      Up = "Up",
      Down = "Down",
      Left = "Left",
      Right = "Right",
    }
  • const enum은 js 코드로 컴파일하면 사라진다.

2) 객체와 다른 점은 무엇일까?

  • enum은 한번 생성되면, 속성 추가 및 수정이 불가하다. 객체는 생성 이후에도 속성을 추가하고 변경할 수 있다.
  • enum은 속성 값으로 숫자, 문자열만 할당할 수 있다. 즉, enum은 JS객체보다 더 엄격하게 타입을 정의하여 사용할 때 유용하다.
  • enum은 언제쓸까?

3) ⭐ keyof, typeof

const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

// key 값을 꺼내오고싶을 때 
type Key = keyof typeof ODirection; // Key를 "Up" | "Down" | "Left" | "Right"로 추론 
// value 값을 꺼내오고 싶을 때
type Value = (typeof ODirection)[keyof typeof ODirection]; // Value를 0 | 1 | 2 | 3 으로 추론
  • type Key = keyof typeof ODirection: 'keyof ODirection'하면 'ODirection'은(는) 값을 참조하지만, 여기서는 형식으로 사용되고 있습니다. 'typeof ODirection'을(를) 사용하시겠습니까? 에러가 뜸-> keyof typeof ODirection
  • 만약에 as const 안하면 type Value를 number로 추론함.


6. 클래스

1) 클래스란?

  • 여러가지 유사한 객체를 쉽게 생성하는 자바스크립트 최신문법

2) TypeScript 클래스

  • 기본문법
    class Person {
      name: string;
      age: number;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    
      sayMyName() {
        console.log(`내 이름은 ${this.name}`);
      }
    }
    
    const eunsu = new Person('은수', 25);
    eunsu.sayMyName()
  • 상속
    class Person {
      name: string;
      age: number;
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    
      sayMyName() {
        console.log(`내 이름은 ${this.name}`);
      }
    }
    
    class Developer extends Person {
      skill: string;
      constructor(name: string, age: number, skill: string) {
        **super(name, age);**
        this.skill = skill;
      }
      coding() {
        console.log(`${this.skill} is Fun`);
      }
    }
    const eunsu = new Developer("은수", 25, "TypeScript");
    eunsu.coding();

3) 클래스 접근 제어자

  • TypeScript의 Public, Private, Protected
    • public: 클래스 안에 선언된 속성과 메서드를 어디서든 접근할 수 있게 한다.
    • private: 클래스 코드 외부에서 클래스의 속성과 메서드를 접근할 수 없다.
    • protected: protected로 선언된 속성이나 메서드는 클래스 코드 외부에서 사용할 수 없으나, 상속받은 클래스에서는 사용할 수 있다.
  • TypeScript의 private과 JavaScript의 private
    class A {
      #a: string = "123"; // js의 private
      private b: number = 123; // ts의 private
      method() {
        console.log(this.#a, this.b);
      }
    }
    • js에서는 protected는 지원안하기 때문에 정교하게 쓰고싶다면 ts의 private, protected 속성을 쓰자
    • ts의 private은 js로 컴파일하면 public으로 바뀐다. 하지만 private 변수에 접근하면 타입스크립트 단계에서 에러가 난다.

4) Implements

  • class는 interface를 구현(implements)할 수 있다, interface가 있으면 class는 그 interface를 따라야한다.
    interface A {
      readonly a: string;
      b: string;
    }
    
    class B implements A {
      a: string = "123";
      b: string = "world";
    }
    
    class C extends B {}
    new C().a;
    new C().b;
    → js로 컴파일하면 interface는 사라지고, implements도 사라짐.

5) 추상클래스: 클래스를 대충 모양만 미리 만들어둔 것

  • abstract키워드를 사용해서 추상 클래스를 생성가능하며, new를 이용하여 객체를 생성할 수 없다.
  • extends를 통해서 자식클래스를 만들 수만 있다.
  • 추상클래스 내 존재하는 추상 메소드는 상속받은 클래스(자식)에서 꼭 구현해야한다 (아니면 에러)
  • 모든 자식이 같은 메소드를 가지지만, 구체적인 내용은 자식마다 다를 수 있다.
    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("do something!");
      }
    }
    
    const z4 = new Bmw("black");


7. 제네릭

1) 제네릭이란?

  • 제네릭은 타입을 미리 정의하지 않고 사용하는 시점에 원하는 타입을 정의해서 쓸 수 있는 문법
  • 함수의 인자에 넘긴 값을 함수의 파라미터로 받아 함수 내부에서 그대로 사용하는 방식

2) 제네릭 기본문법

function getText<T>(text: T): T {
  return text;
}

const getText: <T>(text: T) => T = (text) => {
  return text;
};

const getText = function <T>(text: T): T {
  return text;
};

3) 왜 제네릭을 사용할까?

  • 중복되는 타입 코드의 문제점
  • any를 쓰면 되지 않을까? → any는 자바스크립트 코드 처럼 모든 타입을 다 취급할 수 있는 대신, 타입스크립트의 코드 자동 완성이나 에러의 사전 방지 혜택을 받지 못함

4) ⭐ 제네릭의 타입제약

  • extends를 사용한 타입제약
    // 제네릭의 타입을 string으로 제약한 코드
    function embraceEverything<T extends string>(thing: T): T {
      return thing;
    }
    
    // length 속성을 갖는 타입만 취급
    function lengthOnly<T extends { length: number }>(value: T) {
      return value.length;
    }
  • keyof를 사용한 타입제약
    type DeveloperKeys = keyof { name: string; skill: string };
    
    function printKeys<T extends keyof { name: string; skill: string }>(value: T) {
      console.log(value);
    }
  • 제한 둘 수 있는 형태
    <T extends {...}>
    <T extends any[]>
    <T extends (...args: any) => any>
    <T extends abstract new (...args: any)=> any>

5) 제네릭을 처음 사용할 때 주의해야할 사고방식

function printTextLength<T>(text: T){
	console.log(text.length) // 'T' 형식에 'length'속성이 없습니다.
}
  • 함수에 문자열을 넘겼으니 당연이 이 문자를 받아서 처리하는 함수 내부에서도 문자열로 취급되겠지’ 라고 생각하는 것은 타입스크립트 컴파일러가 아닌 개발자의 관점
  • 컴파일러 관점에서 printTextLength() 함수에 어떤 타입이 들어올지 모르기 때문에 함부로 이 타입을 가정하지 않음
  • 따라서 함수안에 제네릭으로 지정된 text 파라미터를 다룰 때 코드 자동완성이나 타입이 미리 정의된 효과는 얻을 수 없음

6) 기타

function add<T extends string>(x: T, y: T): T {
  return x + y;
}
// 'string' 형식은 'T' 형식에 할당할 수 없습니다

const result = "Hello" + " World"; 
// result의 타입은 "Hello World"가 됨

여기서 result의 타입은 실제 문자열 값인 "Hello World"가 된다. 이것은 TypeScript가 리터럴 타입으로 정확하게 타입을 추론하는 특성이다. 따라서 제네릭 타입 T가 문자열일지라도, + 연산자를 사용하여 문자열을 결합하면 TypeScript는 결과를 string이 아닌 문자열 리터럴 타입으로 간주한다.

profile
🙌꿈꾸는 프론트엔드 개발자 신은수입니당🙌

0개의 댓글