TypeScript 간단 정리 - 3

TypeScript 간단 정리 - 3

Union 타입 / Type Alias

유니온(Union) 타입 : (A || B)

자바스크립트의 OR 연산자 || 와 같은 의미

const printOut = (input: string | number) => {
	console.log(input);
}

printOut('문자열');
printOut(20);
printOut(true);

간단하게 input이라는 파라미터를 받아서 해당 파라미터를 출력해주는 함수이다.

이 함수의 파라미터 input에는 문자열 타입 또는 숫자 타입이 올 수 있다. 함수 호출을 보면 stringnumber는 할당이 되는데 booleantrue 를 할당할때는 오류가 나는 것을 확인할 수 있다.

| 연산자를 이용하여 타입을 여러 개 연결하는 방식

유니온 타입의 장점

function getAge(age: any) {
  age.toFixed(); // Error
  return age;
}

getAge('20');
getAge(20);

위 코드에서 toFixed는 소수점 관련된 메소드 다 보니 age 가 숫자일 때만 실행이 가능하고 다른 자료형이 들어오면 에러가 발생한다.

이럴 때 유니온 타입을 활용할 수 있다.

function getAge(age: number | string) {
  if (typeof age === 'number') {
    age.toFixed();
    return age;
  }
  if (typeof age === 'string') {
    return age;
  }
}

getAge('20');
getAge(20);

예를 들어 getAge라는 함수는 age파람에 numberstring만 들어올 수 있다라고 가정하면 유니온 타입을 활용해서 위와 같이 쓸 수 있다. 그리고 JS 의 typeof 연산자를 활용해서 숫자일때 string 일때 처리를 따로 해줄 수 있다.

function padLeft(value: string, padding: any) {
  if (typeof padding === 'number') {
    return Array(padding + 1).join(' ') + value;
  }
  if (typeof padding === 'string') {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

console.log(padLeft('Hello world', 4)); // "    Hello world"
console.log(padLeft('Hello world', '!!!')); // "!!!Hello world"
console.log(padLeft('Hello world', true)); // Error

현재 함수의 param padding 타입이 any 이기 때문에 컴파일 타임에서 에러가 발생하지 않는다.

하지만 런타임에서 true 와 같은 자료형이 즉, number, string이 아닌경우가 들어가면 에러가 발생한다. 이럴때는 유니온타입으로 string | number 이렇게 써줘서 에러 상황을 컴파일 타임에서 알 수 있다.

Type Alias (사용자 정의 타입)

타입 별칭은 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미한다.

const hero1: { name: string; power: number; height: number } = {
  name: '슈퍼맨',
  power: 1000,
  height: 190,
};

const printHero = (hero: { name: string; power: number; height: number }) => {
  console.log(hero.name, hero.power);
};

console.log(printHero(hero1));

매번 타입을 새로 작성하는 건 번거롭고 재사용이 불가능하다.

type 키워드를 사용해보자

// type.ts

type Hero = {
	name: string;
	power: number;
	height: number;
};
import type { Hero } from './type';

const hero1: Hero = {
  name: '슈퍼맨',
  power: 1000,
  height: 190,
};

const printHero = (hero: Hero) => {
  console.log(hero.name, hero.power);
};

console.log(printHero(hero1));

위 코드처럼 자주 쓰이는 자료형들을 묶어서 관리를 할 수 있다.

type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';

const printDirection = (direction: Direction) => {
  console.log(direction);
};

printDirection('UP');
printDirection('UP!'); //Error

위 코드처럼 string 이라는 타입에서 타입을 'UP' | 'DOWN' | 'LEFT' | 'RIGHT' 로 좁힐 수 있다.

물론 type을 string 으로 할 수도 있지만, 4가지 방향을 의미하는 Direction type이 굳이 string 을 타입으로 가질 필요는 없다.

interface & Intersection Type

JAVA 같은 다른 언어에서 인터페이스는 클래스를 구현하기 전에 필요한 메서드를 정의하는 용도로 쓰이지만, TS에서는 좀 더 다양한 것들을 정의하는데 사용된다.

기본 정의

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

const person1: Person = { name: 'js', age: 20 };
const person2: Person = { name: 'ljs', age: 'twenty' }; // Error

선택 속성

interface Person {
  name: string;
  age?: number;
}

const person1: Person = { name: 'js' };

함수 타입에서 배운 optional parameter와 유사하다. ? 붙여주면 아래 person1 이라는 objectage 가 들어가도 되고 안들어가도 된다.

Read only 속성

interface Person {
  readonly name: string;
  age?: number;
}

const person1: Person = { name: 'js' };
person1.name = 'ljs'; // Error

let readOnlyArr: ReadonlyArray<number> = [1,2,3];
readOnlyArr.splice(0,1); // Error
readOnlyArr.push(4); // Error
readOnlyArr[0] = 100; // Error

읽기 전용 속성은 인터페이스로 객체를 처음 생성할 때만 값을 할당하고 그 이후에는 변경할 수 없는 속성을 의미한다.

문법은 다음과 같이 readonly 속성을 앞에 붙인다. 수정하려고 하면 위와 같이 오류가 난다.

배열을 선언할 때 ReadonlyArray<T> 타입을 사용하면 읽기 전용 배열을 생성할 수 있다.

index type

interface Person {
  readonly name: string;
  [key: string]: string | number;
}

const p1: Person = { name: 'js', birthday: '비밀', age: 20 };

interface에서 속성 이름을 구체적으로 정의하지 않고 어떤 값의 타입만 정의하는 것을 인덱스 타입이라고 한다.

[key: string] ← key에 어떤 string 이든 들어올수있다를 의미한다.

따라서 이 interface index 키에는 string이 들어오고 value에는 string or number가 들어올 수 있다는 의미이다.

추가로 인덱스(키)에는 무조건 string , number만 들어올 수 있다.

함수 타입

interface Print {
  (name: string, age: number): string;
}
// type Print = (name: string, age: number) => string;

const getNameAndAge: Print = function (name, age) {
  return `name: ${name}, age: ${age}`;
};

interface로 함수 타입을 정의할 수 있다. Print interface 를 보면 왼쪽에 매개 변수를 입력하고 오른쪽에 반환 타입을 입력했다. (타입도 마찬가지)

인터페이스 확장

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

interface Korean extends Person {
  birth: 'KOR';
}

interface Korean {
  name: string;
  age: number;
  birth: 'KOR';
}

interface Developer {
  job: 'developer';
}

interface KorAndDev extends Korean, Developer {}

interface KorAndDev {
  name: string;
  age: number;
  birth: 'KOR';
  job: 'developer';
}

인터페이스를 확장해서 새로운 인터페이스를 만들 수 있다. extends라는 키워드로 확장해서 새로운 interface를 만든다.

Korean interface를 예로 설명하면 확장은 Person 인터페이스를 그대로 활용하면서 괄호 안에 있는 type를 추가한 새로운 인터페이스 Korean를 만들겠다라는 의미이다.

intersection Type: 여러 타입을 모두 만족하는 하나의 타입

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

interface Developer {
	name: string;
  skill: string;
}

type DevJob = Person & Developer;

const nbcPerson: DevJob = {
  name: 'a',
  age: 20,
  skill: 'ts',
};

PersonDeveloper 라는 인터페이스가 있다고 할 때, 이 두 인터페이스를 intersection type를 활용하여 하나로 합칠 수 있다.

DevJob type를 가지는 nbcPerson obj를 보면 PersonDeveloper 가 가지고 있는 모든 속성을 가지고 있는 것을 알 수 있다.

아래 벤다이그램 참고

type vs interface

타입 별칭과 인터페이스의 가장 큰 차이점은 타입의 확장 가능 / 불가능 여부이다. 인터페이스는 확장이 가능한데 반해 타입 별칭은 확장이 불가능하다.

따라서, 가능하다면 type 보다는 interface로 선언해서 사용하는 것을 추천한다.