[TypeScript] 타입스크립트랑 데이트 중 🥳

hyeonbin·2023년 12월 13일

FE-log

목록 보기
7/9
post-thumbnail

📃 TypeScript

💡 타입 별칭 (Type Alias)

  • 타입 별칭은 새로운 타입을 정의하며, 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미한다.
  const name: string = '덕배';

  // 타입 별칭 사용
  type MyName = string;
  const name: Myname = '덕배';

  • 타입으로 사용한다는 것에 있어, 타입 별칭은 인터페이스와 유사하다.

  • 인터페이스와 타입 별칭을 비교해보자!

  // 인터페이스
  interface Me {
    name: string,
    age?: number
  }

  // 빈 객체를 Me 타입으로 지정
  const me = {} as Me;
  person.name = '덕배';
  person.age = 30;
  person.address = 'Seoul'; // Error
  // 타입 별칭
  type Me = {
    name: string,
    age?: number
  }

  // 빈 객체를 Me 타입으로 지정
  const me = {} as Me;
  person.name = '덕배';
  person.age = 30;
  person.address = 'Seoul'; // Error

  • 2개의 가장 큰 차이점은 타입의 확장 가능 / 불가능 여부다.

  • 다시 말해, 인터페이스는 extends 또는 implements될 수 있지만, 타입 앨리어스는 extends 또는 implements될 수 없다. 상속을 통해 확장이 필효다면 인터페이스가 유리하다.

  • 그러면 언제 타입 별칭을 사용할까? 그건 바로 인터페이스로 표혈할 수 없거나 유니온 또는 튜플 등을 사용해야 한다면 타입 별칭을 사용하는 게 유리하다.

  // 문자열 리터럴로 타입 지정
  type Str = '덕배';

  // 유니온 타입으로 타입 지정
  type Union = string | null;

  // 문자열 유니온 타입으로 타입 지정
  type Name = '덕배' | '덕수';

  // 숫자 리터럴 유니온 타입으로 타입 지정
  type Num = 1 | 2 | 3 | 4 | 5;

  // 객체 리터럴 유니온 타입으로 타입 지정
  type Obj = {a: 1} | {b: 2};

  // 함수 유니온 타입으로 타입 지정
  type Func = (() => string) | (() => void);

  // 인터페이스 유니온 타입으로 타입 지정
  type Shape = Square | Rectangle | Circle;

  // 튜플로 타입 지정
  type Tuple = [string, boolean];
  const t: Tuple = ['', '']; // Error


💡 유니온 타입 (Union Type)

  • 유니온 타입은 자바스크립트의 OR 연산자 || 와 같이 "A 이거나 B 이다" 라는 의미의 타입이다.

  • 타입스크립트에서는 | 연산자를 사용해 둘 이상의 타입 중 하나를 나타내며, 타입을 여러 개 연결하는 방식으로 사용된다.

  function unFunc(username: string | number) {
    // ...
  }

  // 혹은

  type Username = string | number;

  const me: Username = '덕배'; // 유효
  const me2: Username = 1980; // 유효

⚙️ 유니온 타입 주의!

  • 논리적으로 보면 유니온 타입은 OR, 인터섹션 타입은 AND라고 생각하겠지만, 인터페이스와 같은 타입을 다룰 때는 논리적 사고를 주의해야 한다.

  • 아래 예제를 보면, all 함수의 파라미터 me 타입을 Teacher와 Student 인터페이스의 유니온 타입으로 정의했지만, 이렇게 되면 파라미터의 타입이 Teacher도 되고 Student도 될 수 있다.

  • 함수 내부에서는 name 속성에는 정상적으로 접근할 수 있지만, age와 class 속성에는 접근할 수 없다. 왜? all 함수가 호출되는 시점에서 me의 구체적인 타입이 런타임에 확정되지 않기 때문이다.

  • 결론은, all 함수 안에서는 별도의 타입 가드를 이용해 타입의 범위를 좁히지 않으면 기본적으로 Teacher와 Student 두 타입은 공통적으로 들어있는 속성인 name만 접근할 수 있게 된다.

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

  interface Student {
    name: string;
    class: number;
  }
  
  function all(me: Teacher | Student) {
    me.name;  // 정상 작동
    me.age;   // 타입 오류
    me.class; // 타입 오류
  }
  
  const one: Teacher = { name: '덕배', age: 30 };
  all(one); // all 함수 안에서 me.class 속성을 접근하고 있으면 함수에서 에러 발생

  const two: Student = { name: '덕수', class: 3 };
  all(two); // all 함수 안에서 me.age 속성을 접근하고 있으면 함수에서 에러 발생


💡 인터섹션 타입 (Intersection Type)

  • 인터섹션 타입은 & 연산자를 이용해 여러 개의 타입 정의를 하나로 합치는 방식을 의미한다.

  • 예제를 보면, Teacher 인터페이스의 타입 정의와 Student 인터페이스 타입 정의를 & 연산자를 이용해 합친 후, All이라는 타입에 할당한 것이다.

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

  interface Student {
    name: string;
    class: number;
  }

  type All = Teacher & Student;

  // All 타입은 아래와 같이 정의됨
  {
    name: string;
    age: number;
    class: number;
  }


💡 제네릭 (Generic)

  • 제네릭은 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.

  • 제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시해 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 방식이다. 한번의 선언으로 다양한 타입에 재사용이 가능하다.

  • T 는 제네릭을 선언할 때 관용적으로 사용되는 식별자로 타입 파라미터(Type parameter)라고 한다.

  • 아래 예제를 보면, all 함수 이름 바로 뒤에 <T> 타입 매개변수를 선언했고, 함수의 인자와 반환 값에 모두 T 라는 타입을 추가했다. 이를 활용해 함수의 인자 타입과 반환 타입을 지정한다.

  • 이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 되니까, 함수의 입력값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.

  • 선언한 함수는 2가지 방법으로 호출할 수 있다.

  function all<T>(me: T): T {
    return me;
  }

  // 1. 타입을 명시적으로 지정
  let output = all<string>('나는 나야'); 
  // 2. 타입이 추론되어 명시적으로 지정하지 않아도 됨
  let outputInferred = all('나는 나야'); 

  console.log(output); // 나는 나야
  console.log(outputInferred); // 나는 나야

⚙️ 제네릭을 사용하는 이유

  • 아래 예제를 보면, 인자를 하나 넘겨 받아 반환해주는 함수인데 첫번째 all 함수의 인자와 반환 값이 모두 string 타입으로 지정되어있고, 두번째 all 함수는 여러 가지 타입을 허용하는 any 타입으로 지정되어 있다.

  • 타입을 바꾼다고 함수의 도작에 문제가 생기는 것은 아니지만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지 알 수가 없다. 왜? any 타입은 타입 체크를 하지 않기 때문이다.

  • 그래서 위에서 설명한 제네릭을 통해 문제점을 해결할 수 있다.

  // 1. 타입 지정
  function all(me: string): string {
    return me;
  }

  // 2. 여러가지 타입 허용
  function all(me: any): any {
    return me;
  }

⚙️ 제네릭 인터페이스

  • 제네릭 인터페이스는 특정한 타입을 정의할 때 미리 지정하지 않고, 나중에 사용할 때 실제 타입을 지정하는 것이다.

  • 제네릭 인터페이스는 객체의 형태를 추상화하고자 할 때 사용되며, T 와 같은 타입 변수를 사용해 주로 여러 타입의 객체가 동일한 구조를 가지되 내부 타입이 다를 수 있는 경우에 활용된다.

  • 아래 예제에서 Car<T>는 T 라는 타입 변수를 가진 제네릭 인터페이스다. numCar 변수는 Car<number>로, strCar 변수는 Car<string>으로 선언되어 각각 value의 타입이 명시된다.

  interface Car<T> {
    value: T;
  }

  let numCar: Car<number> = { value: 30 };
  let strCar: Car<string> = { value: '자동차' };

⚙️ 제네릭 클래스

  • 제네릭 클래스는 클래스를 정의할 때 특정한 타입을 미리 지정하지 않고, 인스턴스를 생성하거나 메서드를 호출할 때 실제 타입을 지정하는 것이다.

  • 제네릭 클래스는 데이터 구조를 추상화하고자 할 때 사용되며, T 같은 타입 변수를 사용해 클래스 전체에 대한 타입을 추상화한다.

  • 아래 예제에서 Car<T> 클래스는 생성자와 getValue 메소드에서 T 타입을 사용하고 있다. numCar와 strCar는 각각 Car<number>와 Car<string>로 선언되어, 각 인스턴스의 타입이 명시된다.

  class Car<T> {
    private value: T;

    constructor(value: T) {
      this.value = value;
    }

    getValue(): T {
      return this.value;
    }
  }

  let numCar = new Car<number>(30);
  let strCar = new Car<string>('자동차');

  console.log(numCar.getValue()); // 30
  console.log(strCar.getValue()); // 자동차


💡 모듈 (Modules)

  • 타입스크립트는 기본적으로 변수와 함수, 타입이 전역적으로 사용되기 때문에, 복수의 ts파일이 있을때, 변수와 타입 사용에 있어 서로 충돌하거나 예기치 않게 다른 파일의 변수를 변경하는 등의 문제를 가질 수 있다.

  • 이러한 문제를 해결하는 모듈은 전역적인 범위가 아닌 독립적인 유효 범위 안에서 실행되며, 모듈 안에서 정의된 변수, 함수, 클래스, 인터페이스 등은 외부에서 사용될 수 없는데, export 키워드를 사용해 모듈을 외부로 내보내고, import 키워드를 사용해 다른 모듈에서 내보낸 항목을 가져온다.

  • 이렇게 타입스크립트 모듈은 ES6 모듈을 기반으로 타입스크립트 정적 타입 시스템과 함께 동작한다. 그럼 JavaScript ES6의 모듈과 TypeScript 모듈을 비교해보자!

  // math.js
  export function add(a, b) {
    return a + b;
  }

  // app.js
  import { add } from './math.js';

  console.log(add(10,3)); // 출력: 13
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

// app.ts
import { add } from './math';

console.log(add(10,3)); // 출력: 13

  • 기본 디폴트 export는 기본적으로 모듈에서 한 번만 사용할 수 있으며, import 하는 쪽에서 해당 항목에 원하는 이름을 부여할 수 있고, 디폴트 export 된 대상을 바로 사용할 수 있도록 한다.

  • 차이점을 보자면, 디폴트 export, import 는 모듈의 핵심 기능을 내보내고 가져올 수 있고, 일반적인 export, import 는 추가적인 기능이나 유틸리티 함수와 같은 보조적인 항목을 처리하는데 사용된다.

  // math.ts
  export default function add(a: number, b: number): number {
    return a + b;
  }

  // app.ts
  import add from './math';

  console.log(add(10,3)); // 출력: 13
// math.ts
export default function add(a: number, b: number): number {
  return a + b;
}

// app.ts
import addddd from './math';

console.log(addddd(10,3)); // 출력: 13


💡 d.ts 파일

  • 타입스크립트 선언 파일 d.ts는 주로 JavaScript 라이브러리나 모듈의 타입 정보를 정의하기 위해 사용되고, 타입 검사를 위한 정보로만 사용된다.

  • 자바스크립트에서 전역 객체인 window 나 document에 대한 타입 정보를 제공하는 Ambient 선언을 만들어 보자!

  // global.d.ts

  // Window 인터페이스를 확장하여 사용자가 추가한 속성을 정의
  interface MyWindow extends Window {
    // 사용자가 추가한 속성
    myProperty: string;
  }

  // myWindow 변수를 전역 변수로 사용할 때 해당 타입을 명시적으로 선언
  declare var myWindow: MyWindow;


  // Document 인터페이스를 확장하여 사용자가 추가한 속성을 정의
  interface MyDocument extends Document {
    // 사용자가 추가한 속성
    myMethod(): void;
  }

  // myDocument 변수를 전역 변수로 사용할 때 해당 타입을 명시적으로 선언
  declare var myDocument: MyDocument;

⚙️ 타입 정의 파일이 필요한 이유

  • 자바스크립트로 작성된 모듈을 타입스크립트에서 불러오거나, 자바스크립트로만 개발된 패키지를 타입스크립트 프로젝트에서 설치해서 사용할 때 문제점이 있다.

  • import로 불러오기만 해서는 모듈 안에 있는 것들의 타입을 알 수 없고, 기본적으로 any 타입으로 불러오게 된다. 이럴 때 필요한 게 타입 정의 파일이다.

⚙️ 타입 정의 파일 만드는 방법

  • 타입스크립트 파일을 컴파일할 때 --declaration 이라는 옵션과 함께 컴파일하면 자바스크립트 파일을 만들면서 d.ts 파일도 함께 생성된다.

  • 아래 예제에서 me 함수 코드를 명령어로 컴파일해 보면, main.js 파일과 함께 main.d.ts 파일이 생성된다.

  // main.ts
  export function me(name: string) {
    console.log(`${name} 하이!`);
  }
  # 명령어
  tsc main.ts --declaration
  // main.d.ts
  export declare function me(name: string): void;

  • 자바스크립트도 해보자! 타입스크립트에는 자바스크립트 문법이 포함되어 있기에 자바스크립트 파일도 타입스크립트 컴파일러로 처리할 수 있다.

  • 위 코드와 동일한 자바스크립트 코드를 명령어로 컴파일해 보면, 만들어진 main.d.ts 파일은 추론할 수 없는 타입에 대해 any 타입이라고 적혀있다.

  // main.js
  export function me(name) {
    console.log(`${name} 하이!`);
  }
  # 명령어
  tsc main.js --declaration --allowJS --outDir dist

  --allowJS: 자바스크립트 파일도 타입스크립트 컴파일러가 처리하도록 하는 옵션
  --outDir: 컴파일 결과물을 지정된 디렉토리에 저장하는 옵션
  → dist라는 폴더로 지정해 준 이유는 main.js의 컴파일 결과는 main.js로 저장되기 때문에, 
  덮어쓰는 걸 막기 위해서!

  그 외에도
  --emitDeclarationOnly: .d.ts 파일만 생성하는 옵션
  --declarationDir: 선언 파일의 출력 디렉토리를 지정하는 옵션
  // main.d.ts
  export declare function me(name: any): void;


💡 인덱싱

  • 자바스크립트에서 객체에 동적으로 프로퍼티를 추가하면 타입스크립트에서 타입 안정성을 보장하지 않는다. 그래서 이러한 동적인 프로퍼티 접근을 지원하기 위해 인덱싱(Indexing)을 제공한다.

  • 타입스크립트에서 인덱싱을 사용하면 객체의 프로퍼티에 접근할 때 동적인 키 값을 사용할 수 있으며, 이를 통해 타입 안정성을 유지할 수 있다.

  • 타입스크립트에서 배열 요소와 객체의 속성을 접근할 때는 인터페이스를 사용하면 된다.

⚙️ 배열 요소 접근

  • 일반적으로 배열 요소에 접근할 때는 배열의 인덱스를 사용한다.

  • 아래는 numbers 배열의 첫번째 요소에 접근해 값을 출력한다.

  const numbers: number[] = [1,2,3,4,5];
  console.log(numbers[0]); // 1

  • 이제 인덱싱을 사용해 보자! StrArray 인터페이스는 배열의 인덱스가 숫자고, 해당 인덱스로 접근했을 때 반환되는 값의 타입은 string임을 정의한다.

  • 따라서 fruits 배열에는 숫자 인덱스로 접근하여 문자열 값을 얻을 수 있다.

  interface StrArray {
    [index: number]: string;
  }

  const fruits: StrArray = ['사과', '바나나', '오렌지'];
  console.log(fruits[0]); // 사과

⚙️ 객체 속성 접근

  • key: string 부분은 인덱스 시그니처이며, 이를 통해 객체에는 string 타입의 키로 모든 종류의 값을 추가할 수 있다.

  • 따라서 person 객체에 job이라는 동적인 프로퍼티를 추가할 수 있게 된다.

  // js
  const person = {
    name: '덕배',
    age: 30
  };

  person.job = '개발자'; // 동적으로 프로퍼티 추가
  // ts
  interface Person {
    name: string;
    age: number;
    [key: string]: any; // 인덱스 시그니처 (Index Signature)
  }

  const person: Person = {
    name: '덕배',
    age: 30
  };

  person.job = '개발자'; // 동적으로 프로퍼티 추가


💡 유틸리티 타입 (Utility Types)

  • 유틸리티 타입은 이미 정의해 놓은 타입을 변환할 때 사용하기 좋다.

  • 아래 4개의 유틸리티 타입 이외에도 많은 유틸리티 타입이 있으며, 필요에 따라 적절한 유틸리티 타입을 선택해 사용할 수 있다.

⚙️ Partial

  • 파셜 타입은 기존 타입의 모든 속성을 선택적으로 만들어준다.
  interface User {
    id: number;
    name: string;
    email: string;
  }

  type PartialUser = Partial<User>;

  // PartialUser를 이용하여 일부 속성만 사용할 수 있음
  const partialUser: PartialUser = { name: 'John' };

⚙️ Pick

  • 픽 타입은 기존 타입에서 지정된 속성만 선택하여 새로운 타입을 만든다.
  interface User {
    id: number;
    name: string;
    email: string;
  }

  type UserWithoutEmail = Pick<User, 'id' | 'name'>;

  // UserWithoutEmail은 id와 name 속성만을 가지는 타입
  const userWithoutEmail: UserWithoutEmail = { id: 1, name: 'John' };

⚙️ Omit

  • 오밋 타입은 기존 타입에서 지정된 속성을 제외한 모든 속성을 가지는 새로운 타입을 만든다.
  interface User {
    id: number;
    name: string;
    email: string;
  }

  type UserWithoutEmail = Omit<User, 'email'>;

  // UserWithoutEmail은 email 속성을 제외한 모든 속성을 가지는 타입
  const userWithoutEmail: UserWithoutEmail = { id: 1, name: 'John' };

⚙️ Readonly

  • Readonly 타입은 기존 타입의 모든 속성을 읽기 전용으로 만든다.
  interface User {
    id: number;
    name: string;
  }

  type ReadonlyUser = Readonly<User>;

  // ReadonlyUser는 모든 속성이 읽기 전용
  const user: ReadonlyUser = { id: 1, name: 'John' };
  user.id = 2; // 에러: 읽기 전용 속성이기 때문에 재할당할 수 없음


💡 맵드 타입 (Mapped Type)

  • 맵드 타입은 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 강력한 기능이다.
  {
    name: string;
    age: string;
  }

  // ↓ 변환

  {
    name: number;
    age: number;
  }

  • 자바스크립트의 map() API 함수를 타입에 적용한 것과 같은 효과를 가진다. 자바스크립트 map API는 배열을 다룰 때 유용한 자바스크립트 내장 API이다.

  • 아래 코드는 3개의 객체를 요소로 가진 배열 arr에 map API를 적용한 코드이며, 배열의 각 요소를 순회하여 객체에서 문자열로 변환하였다.

  const arr = [{id: 1, title: '함수'}, {id: 2, title: '변수'}, {id: 3, title: '인자'}];
  const result = arr.map(function(item) {
  return item.title;
  });
  console.log(result); // ['함수', '변수', '인자']

  • 맵드 타입의 기본 구조는 아래와 같다!
  type MappedType = {
    [Property in ExistringType]: NewTypeTransformtion;
  }

  // Property: 기존 타입의 각 속성을 나타내는 변수
  // ExistringType: 변환하려는 기존 타입
  // NewTypeTransformtion: 새로운 타입을 생성하기 위한 변환 규칙을 정의하는 부분

  • 모든 속성을 선택적으로 만들거나, 읽기 전용으로 변환하는 경우에 맵드 타입을 사용한다.
  // 선택적으로 만드는 맵드 타입
  type PartialType<T> = {
    [Property in keyof T]?: T[Property];
  }

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

  // User 타입의 모든 속성을 선택적으로 만듦
  type PartialUser = PartialType<User>



  // 읽기 전용으로 만드는 맵드 타입
  type ReadonlyType<T> = {
    readonly [Property in keyof T]: T[Property];
  };

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

  // User 타입의 모든 속성을 읽기 전용으로 만듦
  type ReadonlyUser = ReadonlyType<User>;
profile
할 수 있다고 믿는 사람은 결국 그렇게 된다 😄😊

0개의 댓글