`프론트엔드 개발자` 개형이의 벽돌집

타입스크립트 (2) - 타입의 사용. 어떻게 사용할 수 있을까? 본문

타입스크립트

타입스크립트 (2) - 타입의 사용. 어떻게 사용할 수 있을까?

개형이 2022. 5. 11. 00:14

회사 스프린트 일정으로 인해 정신이 없어서 업로드가 늦어져서 아쉽지만,

타입스크립트에서 타입을 어떻게 사용하는지 기록해두고자 한다.

 

 


 

원시 타입 (Primitive type)


기본적으로 알고 있는 타입의 종류이다.

  • string
  • number
  • boolean
  • etc..
let str: string = "string";
let num: number = 100;
let bool: boolean = true;

변수 선언 시 타입을 명시해줄 수 있다. (위의 경우는 굳이 명시를 안해줘도 알아서 타입을 추론한다)

 

 

배열


배열의 타입은 ~~[] 혹은 Array<~~>의 형태로 지정 가능하다.

 

let num_arr: number[] = []
let num_arr1: Array<number> = []
// -> number[]  or  Array<number>

 

또다른 방법은 Type, Interface를 이용하여 배열 타입의 alias를 지정할 수 있다.

// type
type PersonList = string[];

// interface
interface PersonList {
  [index: number]: string;
}

 

 

함수


매개변수의 타입, 반환 값의 타입을 지정할 수 있다.

function getFavoriteNumber(x: number): number {
  return x;
}

 

또다른 방법은 Type, Interface를 이용하여 함수 타입의 alias를 지정할 수 있다.

// type
type EatType = (food: string) => void;

// interface
interface EatType {
  (food: string): void;
}

 

 

객체


객체 타입의 경우 , 혹은 ; 로 구분이 가능하여 지정할 수 있다.

-> ({ x: number; y: number } or { x: number, y: number } )

function printCoord(pt: { x: number; y: number }) 
{ 
  //~~~ 
}

 

 

Type & Interface


위에 예시를 봐도 알 수 있지만, 타입의 별칭으로 Type과 Interface를 사용할 수 있다.

type Point = {
  x: number;
  y: number;
}

interface Point {
  x: number;
  y: number;
}

 

❗Type vs Interface

사용법은 대부분 동일하다. 단, 타입은 생성된 뒤 달라질 수 없지만 인터페이스는 병합하여 확장이 가능하다.

 

일반 확장 예시)

// Interface는 extends를 통해 확장을 통한 새로운 타입 설정 가능
interface Animal {
  name: string
}
interface Bear extends Animal {
  honey: boolean
}

// Type은 교집합을 통한 확장을 통해 새로운 타입 설정 가능
type Animal = {
  name: string
}
type Bear = Animal & {
  honey: boolean
}

 

병합 확장 예시)

// Interface의 경우 동일한 이름으로 타입 선언시 자동 병합
interface Window {
  title: string
}
interface Window {
  title2: string
}

// Type은 위와 같이 사용할 경우 에러가 발생

 

❓ Type과 Interface를 잘 사용하는 법은?

  • Interface는 객체의 모양을 선언하는 데만 사용한다. 따라서 원시 타입 별칭(Primitive type alias)는 Type만 가능하다는 점을 참고하자.
  • 타입스크립트 공식 핸드북 에서는 개인적 선호에 따라 자유롭게 선택해서 사용할 수 있지만 잘 모르겠을 경우에는 interface를 사용해보고 문제가 발생했을 때 type을 사용하라고 나와있다.

 

 

유니언 (Union)


| 를 사용하여 여러 개의 타입을 사용 가능하게 할 수 있다.

function printId(id: number | string) { 
  // 매개변수 id는 number 혹은 string 타입을 가질 수 있다.
}

 

단, 유니언을 다룰 때는 해당 유니언 타입의 모든 멤버에 대하여 유효한 작업일 때에만 허용된다.

function printId(id: number | string) {
  console.log(id.toUpperCase()); // 에러 발생 (string 타입에만 유효한 메소드이기 때문)
	
  // typeof 등으로 분기하여 사용 가능함
  if (typeof id === "string") {
    id.toUpperCase() // 정상
  }
}

-> 위의 예시는 Type Guard를 사용한 것인데 이는 추후 포스팅에 기록할 것이다.

 

 

 

읽기 전용 프로퍼티 (readonly)


readonly를 사용하면 객체가 처음 생성될 때만 수정이 가능하고 이후에는 수정이 불가능하다.

interface Point {
  readonly x: number;
  readonly y: number;
}
let p1: Point = {x: 10, y: 20};
p1.x = 5; // 에러 발생

 

❓ const랑 readonly의 차이는?

변수에는 const를 사용하고 프로퍼티에는 readonly를 사용합니다.

 

 

 

리터럴 타입 (Literal Type)


구체적인 문자나 숫자를 타입으로 지정할 수 있다.

function printText (str: string, alignment: "left" | "right" | "center") 
{ 
  // alignment에 "left" | "right" | "center" 제외한 값이 들어오면 에러 표시
}

 

 

 

 

제네릭 (Generic)


Generic은 타입에 변수를 제공하는 방법이다.

자바의 Generic, C++의 Template 개념과 동일하다고 보면 될 듯 하다.

function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("myString"); 
// 출력 타입은 'string'입니다.

let output = identity("myString"); 
// 출력 타입은 'string'입니다. ===> Generic도 타입 인수를 추론

 

→ 위 예제에서 보면 유저가 주는 타입으로 함수 내에서 사용되는 것을 알 수 있다.

 

type Item<T> = {
  id: T extends string | number ? T : never;
};
// 위에 extends는 상속의 의미로 생각하기 보다는
// 'T가 string이거나 number라면'이라고 생각하면 된다.

const item: Item<boolean> = {
  id: true // 타입 'boolean'은 never 타입에 할당될 수 없다고 나옴
}

→ 위 예제에서 보면 Generic은 conditional type으로 조건을 줄 수도 있다.

 

 

Any & Unknown


unknown은 무조건 타입을 좁혀서 사용해야 하는 의무가 있지만, any는 타입을 좁혀서 사용하지 않아도 허용된다는 차이점이 있다.

 

- Any 예시

function func(a: any): number | string | void {
  a.toString(); // any 타입이므로 문제가 발생하지는 않는다

  if (typeof a === 'number') {
    return a * 10;
  } else if (typeof a === 'string') {
	return `Hello ${a}`
  }
}

 

 

- Unknown 예시

function funcUnknown(a: unknown): number | string | void {
  a.toString(); // unknown 타입이므로 문제가 발생

  // 아래 코드에 대해서는 문제 발생 안함 
  // any 대신 unknown을 사용할 경우 안정성 높일 수 있음
  if (typeof a === 'number') {
    return a * 10;
  } else if (typeof a === 'string') {
	return `Hello ${a}`
  }
}

 

 

 

Obtional Type


옵셔널 타입을 사용하면 값을 필수적으로 넣지 않아도 사용이 가능하다.

type Result1<T> = {
  data?: T; // 값이 없어도 에러 발생 안함
  error?: Error; // 값이 없어도 에러 발생 안함
  loading: boolean;
};

 

 

다음과 같은 경우에서는 Optional type을 사용했을 때 문제가 생긴다.

// r1의 data가 있으면 error는 null이고 loading은 false로 만들고 싶음
type Result1<T> = {
  data?: T;
  error?: Error;
  loading: boolean;
};

declare function getResult1(): Result1<string>;

const r1 = getResult1();
r1.data; // 타입은? string | undefined
r1.error; // 타입은? Error | undefined
r1.loading; // 타입은? boolean

if (r1.data) {
  r1.error; // Error | undefined  --> data의 값이 있지만 에러가 null이 아닐 수도 있음
  r1.loading; // boolean
}

-> 위 예시에서 보여지는 문제는 Optional type을 사용하는 대신 유니온 타입을 사용할 경우 해결할 수 있는데,

유니온 타입과 Type Guard 방식을 활용하여 해결한 예시는 다음 코드에서 확인할 수 있다.

type Result2<T> =
  | { type: 'pending'; loading: true }
  | { type: 'success'; data: T; loading: false }
  | { type: 'fail'; error: Error; loading: false };

declare function getResult2(): Result2<string>;

const r2 = getResult2();

if (r2.type === 'success') {
  r2; // { type: 'success'; data: string; loading: false; }
}
if (r2.type === 'pending') {
  r2; // { type: 'pending'; loading: true; }
}
if (r2.type === 'fail') {
  r2; // { type: 'fail'; error: Error; loading: false; }
}

// 목표했던 사항: r2의 data가 있으면 error는 null이고 loading은 false로 만드는 것을
// 안정성 있게 작성할 수 있습니다.

-> literal type으로 분기를 시켜 data가 string 타입일 때는 에러 값을 받지 않게 할 수 있는 것을 확인했다.

 

 

 

서브 타입


좁은 타입에서 넓은 타입으로의 대입이 가능하다. 반대일 경우는 에러가 발생하게 된다.

let sub1: 1 = 1;

let sup1: number = sub1; 
// -> 좁은 타입에서 넓은 타입으로의 대입은 가능 (리터럴 1은 number의 서브타입)

sub1 = sup1;
// -> 에러. sup1은 number, sub1은 리터럴타입이므로 넓은 타입에서 좁은 타입으로의 대입인 상황

 

let sub2: number[] = [1];
let sup2: object = sub2; 
// -> 배열은 object에 포함이므로 가능 (배열은 object의 서브타입)

sub2 = sup2; // 에러
let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3; 
// -> 튜플은 배열의 포함 (튜플은 배열의 서브타입)

sub3 = sup3; // 에러
let sub4: { a: string, b: number} = { a: "a", b: 1}
let sup4: { a: string | number, b: number} = sub4
// Object의 경우 각각의 프로퍼티가 대응하는 프로퍼티가 같거나 서브타입이어야 대입 가능, 
// Array도 마찬가지로 동작

sub4 = sup4;; // 에러

 

 


 

타입스크립트 서론에 대한 포스팅을 올린 후 해당 포스팅을 올리기까지 시간이 꽤 걸려서 아쉽다.

하지만 영원히 방치하진 않았다는 사실에 우선 만족...

 

이번 포스팅에 타입의 사용에 대해 기록했다면 다음 포스팅에는 타입의 추론에 대해 기록해보고자 한다.

꾸준한 개발자가 되자!! 😎

 

Comments