본 포스팅은 Inflearn(인프런) 이정환님의 한 입 크기로 잘라먹는 Typescript 강의를 참고하여 작성되었습니다.

[Utility Type]

Utility Type

Utility Type이란 Generic, Mapped Type, 조건부 Type 등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 타입을 미리 만들어 놓은 것을 의미한다. 기본적으로 TS에서 자체적으로 지원해주며 가장 많이 사용되는 Utility Type에 대해서 알아보자.

[Mapped Type 기반 Partial, Required, Readonly]

Partial<T>

- 특정 객체 타입의 모든 속성을 Optional Property로 전환해주는 Type이다. 정의되어 있는 타입이 존재하지만 사용자의 활동에 따라서 사용이 될 수도 있고 사용되지 않을 수도 있는 속성들에 대해 정의하기 좋은 타입이다. 예시로는 블로그 작성 시 제목, 글 내용, 태그 등 다양한 속성이 있지만 사용자가 모든 속성을 사용한다는 보장이 없기 때문에 블로그를 떠올리면 이해가 쉽다. 코드 예시를 살펴보자.

interface Post {
  title: string,
  tags: string[],
  content: string,
  thumbnailURL?: string
}

const draft: Partial<Post> = {
  title: "제목",
  content: "내용"
}

type Partial<T> = {
  [key in keyof T]?: T[key]
}

Interface로 정의된 Post에는 4가지의 속성이 존재하지만 draft로 선언된 변수에는 2개의 속성만 할당이 되어있다. 그럼에도 오류가 발생하지 않는 이유는 변수 draft의 타입을 Partial<Post>로 정의하면서 모든 속성을 Optional Property로 바꾸었기 때문에 특정 속성이 존재하지 않아도 오류가 발생하지 않는 것이다.

마지막에 Type Alias로 Partial 타입을 자체적으로 생성해보았다. 적용되는 원리와 순서를 살펴보자.

1. Generic을 이용하여 T에 들어오는 객체의 속성들을 keyof 연산자를 통해서 Union Type으로 추출한다.
2. 다음으로 Mapped Type에서 제공되는 in 연산자를 통해 key를 Union Type으로 추출된 속성들에 하나씩 Mapping이 된다.
3. 그 속성들이 Indexed Access Type인 T[key]를 통해 속성을 추출되어 모든 것이 Optional Property로 전환이 된다. 

 

Required<T>

- 특정 객체 타입의 모든 속성을 필수 속성으로 바꾸어주는 타입이다. Partial Type은 모두 선택적인 속성으로 바꾸어주었다면 Required Type은 모든 속성을 필수로 바꾸어주는 타입이다. 따라서 객체 타입을 초기에 정의할 때 특정 속성을 Optional Property로 정의를 해두었어도 변수를 선언하고 타입을 지정해줄 때 Required Type으로 지정을 해준다면 Optional Property를 포함한 모든 속성이 필수 속성이 되는 것이다. 코드 예시를 함께 살펴보자.

interface Post {
  title: string,
  tags: string[],
  content: string,
  thumbnailURL?: string
}
const A: Required<Post> = {
  title: "title",
  tags: ["tags"],
  content: "content",
} //Error: thumbnailURL Property is neccessary

const B: Required<Post> = {
  title: "title",
  tags: ["tags"],
  content: "content",
  thumbnailURL: "url"
}

type Required<T> = {
  [key in keyof T]-? : T[key]
}

thumbnailURL 속성은 Interface에서 Optional로 정의가 되었음에도 Required Type으로 인해 필수 속성으로 적용되는 것을 볼 수 있다.

하단에는 자체적으로 Required Type을 생성하는 코드인데 코드의 문법은 앞서 살펴본 Partial Type에서와 동일하지만 선택적 속성이 아닌 필수 속성으로 만들어주기 위해서는 "?"가 아닌 "-?"를 붙여주어야 한다.

 

Readonly<T>

- 특정 객체 타입의 모든 속성을 읽기 전용 속성으로 바꾸어주는 타입이다. 앞서 두가지 타입을 살펴보았으니 어떤 원리로 적용되고 생성되는지는 이해가 쉬울 것 같으므로 코드 예시만 살펴보자.

interface Post {
  title: string,
  tags: string[],
  content: string,
  thumbnailURL?: string
}
const A: Readonly<Post> = {
  title: "title",
  tags: ["tags"],
  content: "content",
} //Error: thumbnailURL Property is neccessary in Readonly Type

const B: Readonly<Post> = {
  title: "title",
  tags: ["tags"],
  content: "content",
  thumbnailURL: "url"
}

type Readonly<T> = {
  readonly [key in keyof T]: T[key]
}

[Mapped Type 기반  Pick, Omit, Record]

Pick<T,K>

- 객체 타입으로부터 특정 속성만 골라내는 타입이다. Generic으로 두가지를 정의하는데, T는 속성을 골라낼 객체 타입을 의미하고 K는 객체에서 골라낼 속성들을 의미한다. 아래 코드 예시를 보자.

interface Post {
  title: string,
  tags: string[],
  content: string,
  url?: string
}
const PostA: Pick<Post, "title" | "content"> = {
  title: "title",
  content: "content"
}

type Pick<T,K extends keyof T> = {
  //K extends 'title'|'tags'|'content'|'url'
  //'title'|'content' extends 'title'|'tags'|'content'|'url'
  [key in K]: T[key];
}

Interface로 정의된 Post 타입은 4가지 속성을 갖고 있지만 선언한 변수 PostA는 2가지 속성만 사용을 하고자 한다. 이 때 Pick Type을 사용해서 사용할 속성만 골라낼 수 있는 것이다.

하단에 작성된 Pick Type을 생성하는 방법을 보면 앞서 살펴보았던 keyof 연산자를 사용하지 않고 extends 키워드와 keyof 연산자를 함께 사용하여 K가 T의 속성들의 Serve Type이라는 것을 명시해주며 동작하게 된다.

 

Omit<T,K>

- 객체 타입으로부터 특정 속성을 제거하는 타입이다. Omit Type은 Pick Type과 반대되는 타입으로 Pick Type의 단점을 보완할 수 있는 타입이다. 예를 들어 특정 객체의 속성이 많은 경우에 Pick Type으로 필요한 속성을 모두 빼내어 사용하는 것보다 필요하지 않은 속성을 제거해두고 사용하는 것이 편리한 경우가 있을 것이다. 그러한 경우에 사용하는 타입이다.

interface Post {
  title: string,
  tags: string[],
  content: string,
  url?: string
}
const PostB: Omit<Post,"title"> = {
  tags: ["ts"],
  content: "content",
  url: "url"
}

type Omit<T,K extends keyof T> = Pick<T, Exclude<keyof T,K>>
//1.Omit<Post,"title"> = Pick<Post, Exclude<keyof Post, "title">
//2.Omit<Post,"title"> = Pick<Post, Exclude<'title','tags',content','url', "title">
//3.Omit<Post,"title"> = Pick<Post, 'tags',content','url'>
//4.Omit<Post,"title"> = Pick<Post, 'tags',content','url'>

사용 예시는 위와 같으며 직접 Omit Type을 작성하는 과정도 하단에 적혀있는데 동작의 순서를 살펴보면 Pick Type과 Omit Type이 반대되는 것을 확인해볼 수 있다.

 

Record<K,V>

- 동일한 속성 패턴을 갖는 객체 타입을 정의하는데 사용하는 타입이다. 내부 속성은 모두 같지만 외부 속성들의 이름이 다른 다중 객체에서 사용하기 용이하다. 코드 예시를 한 번 보자.

type A = {
  small: {
    url: string
  },
  medium: {
    url: string
  },
  large: {
    url: string
  },
  xlarge: {
    url: string
  },
}
type B = Record<
  "small" | "medium" | "large" | "xlarge",
  { url: string; size: number }
>;

type Record<K extends keyof any, V> = {
  [key in K]: V;
}

K로는 적용할 외부 속성들을 Union Type으로 입력하고 V에는 삽입할 내부 속성을 입력하면 된다.

하단에 자체적으로 생성하는 Record Type 또한 확인해 볼 수 있다.

[Exclude, Extract, Return Type]

Exclude<T,U>

- T에서 U를 제거하는 타입을 의미한다. 코드 예시로 살펴보며 과정을 이해해보자.

type A = Exclude<string | boolean, boolean>;
/**
 * 1단계
 * Exclude<string, boolean>
 * Exclude<boolean, boolean>
 * 2단계
 * string | never
 * 3단계
 * string
 */
type Exclude<T,U> = T extends U ? never : T;

 

Extract<T,U>

- T에서 U를 추출하는 타입을 의미한다.  코드 예시로 살펴보며 과정을 이해해보자.

type B = Extract<string | boolean, boolean>
type Extract<T,U> = T extends U ? T : never;

 

ReturnType<T>

- 함수의 반환값의 타입을 추출하는 타입을 의미한다.  코드 예시로 살펴보며 과정을 이해해보자.

function funcA(){
  return "string"
}
function funcB(){
  return 10
}
type ReturnA = ReturnType<typeof funcA>
type ReturnB = ReturnType<typeof funcB>

type ReturnType<T extends (...args: any) => any> = T extends (...args:any) => infer R ? R : never;

'Typescript' 카테고리의 다른 글

[TypeScript] - (9) 조건부 타입  (1) 2024.08.22
[TypeScript] - (8) Type 조작  (0) 2024.08.14
[TypeScript] - (7) Generic(제네릭)  (2) 2024.08.07
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
본 포스팅은 Inflearn(인프런) 이정환님의 한 입 크기로 잘라먹는 Typescript 강의를 참고하여 작성되었습니다.

[조건부 타입]

조건부 타입이란 JS의 삼항 연산자를 사용해서 타입을 정의하는 방법을 말한다. Extends 키워드와 삼항 연산자를 함께 사용하여 해당 타입이 특정 타입의 확장을 받아 참, 거짓을 판별한 후 타입을 지정해주는 방식이다. 원시 타입으로 예시를 들면 다음과 같다.

type A = number extends string ? string : number; //typeof A === number

type ObjA = {
  a:number
}
type ObjB = {
  a:number,
  b:number
}
type B = ObjB extends ObjA ? number : string //typeof B === number

A의 경우, number 타입은 string 타입의 서브 타입이 아니기 때문에 삼항 연산자를 통해서 나온 결과 false에 의해서 number가 된다.

B의 경우 ObjB 타입은 ObjA 타입의 서브 타입이므로 삼항 연산자를 통해 나온 결과는 true로 B는 number 타입이 된다.

하지만 조건부 타입은 이런 원시 타입 같은 기본적인 경우에서는 사용하지 않는다.

 

조건부 타입은 일반적으로 제네릭과 함께 사용하여 가변적이고 논리적인 흐름에 따라서 사용하기에 유용하다.

제네릭과 함께 사용한 경우는 아래와 같은 형태이다.

type Switch<T> = T extends number ? number : string;

let a: Switch<number>;
let b: Switch<string>;

제네릭에 의해 정의된 타입에 따라서 변수의 타입을 결정 짓게 된다.

또한 함수에서도 함수 오버로딩을 이용해서 오버로딩 시그니쳐와 구현 시그니쳐를 함께 사용하여 사용이 가능하다.

function removeSpaces<T>(text:T): T extends string ? string : undefined;
function removeSpaces(text:any) {
  if(typeof text === "string"){
    return text.replaceAll(" ", "");
  }else{
    return undefined
  }
}
let result = removeSpaces("Hello World");
result.toUpperCase();

let result1 = removeSpaces(undefined);

가장 먼저 함수 오버로딩을 통해서 오버로딩 시그니쳐를 구현하고 그에 맞게 구현 시그니쳐를 작성하였다. any로 정의되어 있는 매개변수인 text의 타입이 무엇인가에 따라 조건부로 타입이 결정되어 로직에 맞게 반환값을 사용할 수 있게 된다.

text의 타입이 string이라면 조건부 타입과 제네릭을 통해 text는 string 타입으로 정의되어 String Type Method를 사용가능하게 되고, undefined인 경우는 undefined 타입으로 정의되어 undefined를 반환하게 된다.

 

[분산적 조건부 타입]

분산적 조건부 타입이란 앞서 살펴본 조건부 타입과 제네릭을 사용하는 경우에서 특정 변수를 선언할 때 변수의 타입이 Union Type으로 정의된 경우 타입이 정의되는 방식을 의미한다. 아래의 간단한 코드를 보자.

type A<T>: T extends number ? number : string;
let a:A<number>; //typeof a === number
let b:A<string>; //typeof b === string
let c:A<number | string>; //?

위의 코드에서 변수 a,b는 앞서 살펴본 제네릭과 조건부 타입의 혼용 방식으로 타입이 정의되었다. 하지만 변수 c의 경우에는 제네릭 타입이 Union Type으로 정의가 되어있다. 이러한 경우는 어떻게 적용될까.

이런 경우 적용되는 방식이 바로 분산적 조건부 타입이다. 제네릭 문법에 의해서 Union Type으로 정의된 타입이 제네릭에 적용되어 타입을 판단하는 것이 아니라 Union Type을 분산적으로 판단한다. 위의 코드를 예시로 보자면 number 타입이 먼저 제네릭을 거쳐서 조건부 타입이 적용되고 그 이후 순차적으로 string 타입이 제네릭을 거쳐서 조건부 타입을 거친 후 두 가지 결과값이 만나 다시 Union Type으로 합쳐지는 것이다. 분산적 조건부 타입의 생성 과정을 코드로 살펴 보자.

type Switch<T> = T extends number ? string : number;

let c:Switch<number | string>;
/*
* 1.Switch<number> => string
* 2.Switch<string> => number
* typeof c === <string | number>
*/

let d:Switch<boolean | number | string>;
/*
* 1.Switch<boolean> => number
* 2.Switch<number> => string
* 3.Switch<string> => number
* typeof d === <string | number>
*/

위와 같이 Union Type으로 정의된 타입을 하나씩 제네릭에 적용되어 조건부 타입을 거친 후 반환된 타입들이 다시 Union Type으로 생성되어 변수의 타입으로 정의가 되는 것이다.

실용적으로 사용되는 예제를 다음 코드로 한 번 이해해보자.

type Exclude<T,U> = T extends U ? never : T;
type A = Exclude<number | string | boolean , string>
/**
 * 1단계
 * Exclude<number, string>
 * Exclude<string, string>
 * Exclude<boolean, string>
 * 2단계
 * number
 * never
 * boolean
 * 3단계
 * <number | boolean>
 */
type Extract<T,U> = T extends U ? T : never;
type B = Extract<number | string | boolean, string>;
/**
 * 1단계
 * Extract<number, string>
 * Extract<string, string>
 * Extract<boolean, string>
 * 2단계
 * never
 * string
 * never
 * 3단계
 * string
 */

Union Type으로 정의된 타입과 제네릭으로 두가지 타입을 받았을 경우 분산적으로 타입 정의가 어떻게 진행되는지 과정을 함께 작성하였다. 위의 예시에서 never 타입은 모든 타입의 서브 타입으로 별도로 적용이 되지 않기 때문에 자동적으로 사라지는 것을 볼 수 있다.

분산 방지

분산적 조건부 타입에서 타입의 분산을 방지하는 방법도 존재한다. 간단하게 제네릭 타입에 대괄호를 씌워주면 된다.

type Exclude<T,U> = [T] extends [U] ? never : T;
type A = Exclude<number | string | boolean , string> //typeof A === string | number | boolean

위와 같이 제네릭에 해당하는 T와 U에 대괄호를 씌워주면 분산적으로 타입 정의가 되지 않아 Union Type으로 정의된 타입이 하나로 묶여 조건부 타입에 적용이 되는 것을 볼 수 있다.

 

[Infer]

Infer란 Inference라는 단어의 약자로 추론을 의미한다. 이 말은 Infer라는 키워드를 통해 타입을 추론하여 가져올 수 있다는 것을 의미한다. 우리가 함수를 사용할 때 만약 함수의 반환값의 타입만 가져와서 다른 타입으로 적용을 하고 싶은 경우 어떻게 처리를 해 줄 수 있을까. 아래 코드와 같이 사용할 수 있을 것 같다.

type Func = () => string;
type Return<T> = T extends () => string ? string : never;

type A = Return<Func>; // typeof A === string

하지만 다양한 함수의 타입이 정의되어 있고 함수의 반환값의 타입을 반환해주는 타입이 위와 같이 Return처럼 하나만 존재하는 경우는 어떻게 할까. 이런 경우에 Infer을 사용한다.

type Func = () => string;
type FuncA = () => number;
type Return<T> = T extends () => infer R ? R : never;

type A = Return<Func>; // typeof A === string
type B = Return<FuncA>; //typeof B === number

위와 같이 우리가 이전에 제네릭에서 T,U 같은 문자를 사용하였듯이 infer과 R이라는 문자를 사용하여 조건부 타입으로 정의하면 제네릭으로 함수 타입을 받게 되고 infer R은 반환값의 타입을 올바르게 반환받을 수 있도록 알맞게 추론하여 반환을 해준다.

정리하면 Type A 같은 경우는 () => string에 맞게 Infer R이 추론을 하여 R에 string이 정의가 되어 반환되는 것이고, Type B 같은 경우는 () => number에 맞게 Infer R이 number로 추론을 하여 R에 number가 정의되어 반환되는 것이다.

하지만 새로운 타입을 정의하면서 아래 예시처럼 제네릭에 특정 타입을 넣어주게 되면 Infer은 타입을 추론하지 못하고 삼항 연산자는 false를 반환하게 되어 조건부 타입에서 Type C에게 never을 정의하게 된다.

type Return<T> : T extends () => infer R ? R : never;
type C = Return<number>; //typeof C === never

간단한 다른 예시도 하나 보면서 마무리 해보자.

type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
/**
 * 1.T는 Promise 타입이어야한다.
 * 2. Promise 타입의 결과값 타입을 반환해야한다.
 */

type PromiseA = PromiseUnpack<Promise<number>>; //typeof PromiseA === number

type PromiseB = PromiseUnpack<Promise<string>>; ////typeof PromiseB === string

'Typescript' 카테고리의 다른 글

[TypeScript] - (10) Utility Type  (1) 2024.09.04
[TypeScript] - (8) Type 조작  (0) 2024.08.14
[TypeScript] - (7) Generic(제네릭)  (2) 2024.08.07
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01

[Indexed Access Type]

- Indexed를 이용해서 다른 타입의 특정 Property 값을 추출하는 방식으로 객체,배열,튜플에서 사용이 가능하다. 특정 변수의 타입을 정의할 때 기존에 정의 되어있던 객체,배열,튜플의 특정 타입만 별도로 사용하고 싶을 때 유용하게 사용된다.

 

Object

이해하기 쉽도록 객체부터 코드로 예시를 들어보겠다.

//Object
interface User {
  name: string,
  age: number,
  info: {
    id: number,
    gender: string
  }
}
// 일반적인 방법
function printInfo(info: {id: number, gender: string}){
  console.log(`${info.id}, ${info.gender}`);
}
//Indexex Access Type
function printInfo(info:User["info"]){
  console.log(`${info.id}, ${info.gender}`);
}
const user: User = {
  name: "Tom",
  age: 10,
  info: {
    id:1,
    gender: "Man"
  }
}

Interface를 통해 객체 타입으로 정의된 User 타입이 존재한다. 해당 타입 정의에 맞춰 user라는 변수도 선언이 되어있다. 또한 User의 info 속성의 타입만 별도로 사용하는 함수 printInfo가 존재한다.

이 때 일반적인 방법으로 info 속성의 타입을 사용하는 경우에 해당 타입에 속성이 추가가 되는 경우는 info 타입을 사용하는 여러가지 함수에게 일일히 매개변수의 타입을 추가해주어야 하는 굉장히 번거롭고 비효율적인 상황이 발생한다. 이런 경우에 Indexed Access Type을 활용하여 특정 속성의 타입을 정의하여 사용하면 자동적으로 해당 속성의 타입을 모두 가져오기 때문에 효율적인 코드를 작성할 수 있게 된다.

여기서 주의해야 할 점이 있다. Indexed Access Type을 사용하는 경우 정의된 타입명 뒤에 대괄호를 사용하는데 대괄호 내에 들어가는 요소는 변수나 일반 문자열이 아닌 타입이 들어가는 것이다. 쌍따옴표(" ")로 인해 문자열로 오해를 하는 경우가 있는데 정확히는 타입이 들어가는 것이기 때문에 해당 타입명과 동일한 변수를 선언하는 경우는 오류가 발생할 수 있으니 주의해야 한다.

또한 위 예시 코드에서 보이듯이 info 속성을 사용하고 있는데 info 속성 타입 내부의 id 속성의 타입만 사용하고 싶은 경우는 배열의 형식에 맞추어 중첩 배열을 사용하면 된다. 이것 또한 코드로 간단히 살펴보자.

//Object
interface User {
  name: string,
  age: number,
  info: {
    id: number,
    gender: string
  }
}
const user: User = {
  name: "Tom",
  age: 10,
  info: {
    id:1,
    gender: "Man"
  }
}
function printInfo(info:User["info"]["id"]){
  console.log(`${info.id}, ${info.gender}`);
}
printInfo(10.23); // 10

 

Array

다음으로는 배열을 사용한 경우를 알아보자.

배열의 경우 기존에 정의되어 있는 타입을 사용하여 생성하고자하는 변수의 타입을 정의하면서 타입 정의 뒤에 대괄호를 이용하여 배열의 index를 가져오듯이 number이나 숫자 값을 넣어주면 된다. 배열 내부에는 동일한 타입의 요소들이 반복이 될 것이므로 아무 index의 타입만 갖고 와서 사용하면 된다. 아래 코드를 보며 이해해보자.

//Array
type A = {
  name: string,
  age: number,
  info: {
    id: number,
    type: string
  }
}[];
const a: A[number] = {
  name: "a",
  age: 10,
  info: {
    id: 1,
    type: "array"
  }
}
function printA(info:A[number]["info"]){
  console.log(`${info.id}, ${info.type}`)
}

A로 정의되어 있는 타입이 존재하며, 변수 a에 타입 A를 정의해주면서 선언을 해주었다. 이 때 타입 A를 정의해주면서 뒤에 대괄호를 통해 number을 넣어주며 특정 index의 타입을 동일하게 사용하겠다는 표현을 해주었다. 따라서 A로 정의된 객체 배열의 요소인 하나의 객체 타입과 동일한 속성과 타입을 이용해서 변수 선언이 가능해졌다. 함수에서도 동일하게 사용되는데 매개변수를 자세히 보면 A["info"]라는 타입은 객체에서 사용하는 Index Access Type 정의 방식이다. 하지만 A 자체는 객체를 요소로 갖는 배열 타입이므로 해당 배열에서 하나의 요소만 가져와 타입으로 사용해야하기 때문에 A 바로 뒤에 [number]을 포함하여 하나의 요소만을 갖고와 객체 타입을 사용할 수 있도록 한 것이다.

 

Tuple

Tuple Type은 상대적으로 더 간단하게 사용할 수 있다.

//Tuple
type tup = [number,string,boolean];
type tup1 = tup[0]; //number
type tup2 = tup[1]; //string
type tup3 = tup[2]; //boolean
type tup4 = tup[number]; //number|string|boolean

위의 코드와 같이 특정 index의 타입을 갖고와 별도로 선언하여 Tuple로 정의되어 있는 특정 타입을 사용할 수 있는 것이다. 마지막 줄의 tup4와 같은 경우는 앞서 살펴본 배열의 Indexed Access Type 정의 방식과 동일한 방식인데 본 방식은 Tuple로 정의되어 있는 타입을 살펴본 후 최적화하여 사용 가능한 타입들을 Union 타입으로 정의해주는 방식이다.

[Operator keyof]

Operator keyof

- 객체 타입에 사용하는 연산자이다. 객체 형태로 정의된 타입의 key 값들을 불러와 사용할 수 있는 연산자이다. 간단한 코드를 보며 살펴보자.

interface User {
  name: string,
  age: number
}
const user:User = {
  name: "tom",
  age: 10
}
function getUser(user:User,key: keyof User){
  return user[key]
}
getUser(user,"name"); //Tom
getUser(user,"age"); //10

위의 코드 예시를 보면 getUser 함수는 user 변수의 특정 key에 해당하는 value 값을 반환하는 함수이다. 이 때 함수의 매개변수로 key를 줄 때 key의 타입을 String Literal Type으로 User 객체 타입이 갖고 있는 key 값을 일일히 작성해 Union 타입으로 설정하는 것도 가능은 하지만 만약 key의 개수가 매우 많은 경우에는 코드의 가독성과 효율성이 떨어지게 된다. 따라서 keyof 연산자를 통해서 key 값을 편리하게 가져올 수 있는 방식이다.

이 때 주의해야할 점은 keyof 뒤에는 반드시 정의된 타입이 와야한다. 그렇지 않으면 연산 오류가 발생하기 때문에 주의해야한다.

Operator typeof

keyof 연산자와 비슷한 형태의 연산자가 존재한다. 바로 typeof 연산자이다. typeof 연산자Type Alias에서 타입 추론으로 사용할 수 있는 방식으로 특정 변수를 생성할 때 변수의 타입을 바로 정의해주는 것이 아닌 정의된 변수로 Type Alias의 타입을 추론하여 타입 정의를 역으로 생성하는 기능을 제공한다. 코드로 보면 이해가 쉬울 것이다.

//typeof
type User = typeof user;
const user = {
  name: "tom",
  age: 10
}
function getUser(user: User, key: keyof typeof User){	//typeof user == User
  return user[key];
}
getUser(user,"age"); // 10

따로 타입이 정의되지 않은 변수 user에 의해 타입 User의 타입이 자동적으로 추론되며 자연스럽게 keyof 연산자와 함께 사용이 가능하기도 하다.

[Mapped Type]

Mapped Type

- 객체 타입을 조작하는 타입이다. 실무에서 굉장히 많이 사용되는 문법중에 하나이고 객체 타입을 조작하는 방법인데도 불구하고 Interface로는 정의가 불가능하여 Type Alias로만 정의를 해야하는 문법이다. 간단한 예시로 객체 타입으로 정의된 Interface 타입과 그 타입을 사용하여 유저를 생성하는 함수와 유저 정보를 수정하는 함수가 존재한다는 코드를 만들어보자. 이 때 Mapped Type이 사용되는 경우는 유저 정보를 수정하는 함수에서 모든 속성을 불러오거나 수정할 필요 없이 원하는 속성만 다룰 수 있도록 하는 경우이다. 아래 코드를 보자.

interface User {
  id: number,
  name: string,
  age: number
}
type newUser = {
  [key in "id"|"name"|"age"] : User[key] //General Mapped Type
}
type newUser1 = {
  [key in "id"|"name"|"age"]? : User[key] //Optional Property
}
type newUser2 = {
  [key in keyof User] : User[key] //Use keyof operator
}
type newUser3 = {
  [key in keyof User] : boolean //Change type
}
type newUser4 = {
  readonly [key in keyof User] : User[key] //Add a readonly keyword
}
function fetchUser():User{
  // ...get logic
  return {
    id: 1,
    name:"tom",
    age:10
  }
}
function updateUser(user:newUser1){
  // ...update logic
}
updateUser({
  age: 10
})

 

하나씩 코드를 살펴보자.

Interface로 정의된 User 객체 타입이 있다. 그리고 유저 정보를 가져오는 함수 fetchUser, 유저 정보를 수정하는 함수 updateUser가 있다. fetchUser 함수는 User 타입에 맞게 데이터를 반환하고 updateUser 함수는 매개변수로 user를 받는다. 이 때 매개변수 user의 타입이 될 수 있는 여러가지 타입을 Mapped Type으로 정의해두었다.

- newUser은 일반적인 Mapped Type 문법을 사용하여 선언한 타입이다. key in 키워드와 Union 타입으로 String Literal 값들을 User의 Indexed Access Type으로 정의한 것이다.
- newUser1은 newUser로 정의된 타입들을 Optional Property로 적용한 문법이다. newUser 타입은 모든 속성이 필수적으로 사용되어야 하기 때문에 유저 정보 수정 시 불필요하게 입력되어야 하는 속성들이 존재하게 되기 때문이다. Optional Property를 사용한다면 해당 속성들을 사용해도 되고 사용하지 않아도 되게 되므로 updateUser 함수의 목적과 상응한다.
- newUser2은 String Literal과 Union 타입으로 정의된 key 값들을 keyof 연산자를 통해 코드 최적화를 한 방법이다.
- newUser3은 keyof 연산자를 통해 접근한 key 값들의 타입을 모두 변경해주는 방법이다. value의 타입에 해당하는 부분에 변경하고자 하는 타입을 넣어주면 된다.
- newUser4는 모든 key 즉 객체의 속성들을 readonly(읽기 전용) 속성으로 변경하는 방법이다. 객체의 속성값들을 안전하게 보호하는데에 사용된다.

[Template Literal Type]

Template Literal Type

- 우리가 쉽게 접할 수 있는 Literal Type으로 정의된 타입을 조합하는 문법이다. Union 타입으로 정의된 타입들을 각각 조합하고 싶을 때 사용하는데 많이 사용되거나 접하게 되는 일이 적기 때문에 간단하게 코드로만 살펴보고 끝내보자.

type A = 'a' | 'b' | 'c';
type B = 'c' | 'd' | 'e';
type AB = `${A}-${B}`

type C = 1 | 2 | 3;
type D = 4 | 5 | 6;
type CD = `${C}-${D}`

'Typescript' 카테고리의 다른 글

[TypeScript] - (10) Utility Type  (1) 2024.09.04
[TypeScript] - (9) 조건부 타입  (1) 2024.08.22
[TypeScript] - (7) Generic(제네릭)  (2) 2024.08.07
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01

[Generic(제네릭)]

Generic(제네릭)

- Generic이란 일반적인이라는 뜻을 가진 단어로 넓게 해석하면 통용적인, 종합적인 의미로 볼 수도 있다. 따라서 Generic이란 어느 한 타입에 국한되지 않고 다양한 형태의 타입을 수용하여 표현할 수 있는 문법이다.

(*다양한 형태의 타입을 유연하게 받아들인다는 특징 때문에 Polymorphism(다형성)에 사용되는 문법이라고도 칭한다.)

Generic의 사용법은 간단하다. 문법 자체를 완벽히 이해하는데에는 살짝 어렵기도 하고 복잡한 느낌이 들 수도 있지만 가시적으로는 코드가 한결 간결해지는 효과를 볼 수 있다.

function func(value:string){
  return value;
}
let a = func(10);

//Generic
function func<T>(value: T): T {
  return value;
}
let num = func(10);	//typeof num === number
let str = func("a");	//typeof str === string
let bool = func(true);	//typeof bool === boolean

간단하게 원리를 살펴보면 <T>의 형태로 Generic을 사용해주면서 정의된 함수를 사용할 때 argument로 받는 값의 타입을 TS가 파악하여 T에 부여되고 T가 반환값에도 할당이 되면서 자연스레 선언한 변수의 타입으로 지정이 되는 것이다.

위의 코드에서 num 변수를 예로 들어보면 func(10)을 통해 num은 10이라는 값을 할당 받았다. 10은 number 타입이므로 value: number로 할당되며 T가 number이라는 타입을 받게 되며 반환값의 타입인 T도 number을 할당받아 변수 num의 타입은 자동으로 number 타입이 되는 것이다.

 

위와 같이 일반적으로 사용하는 방법도 있지만 명시적으로 Generic을 사용하는 방법도 존재한다. 변수를 선언 후 할당을 해줄 때 argument 앞에 Generic을 입력해주는 방법이 있다. 비교를 위해 타입 단언으로 명시적인 타입 정의에 대한 코드를 함께 살펴보자.

function func<T>(value: T): T{
  return value;
}
//타입 단언
let arr = func([1,2,3] as [number,number,number]);
//Generic
let arr1 = func<[number,number,number]>([1,2,3]);

이렇게 사용하는 경우는 argument의 값의 타입인 T에 먼저 할당되는 것이 아닌 함수명 뒤에 붙은 <T>에 먼저 타입이 할당되고 다음으로 argument의 타입과 반환값의 타입 T에 할당이 된다.

 

[타입 변수 응용]

응용.1 - 함수의 arguments의 타입이 다른 경우 Generic 사용

function swap<T,M>(a:T,b:M){
  return [b,a];
}
const [a,b] = swap("1",2);

함수의 arguments로 받는 값의 타입이 다른 경우 Generic을 2개 이상 사용하여 표현이 가능하다.

 

응용.2 - argument가 배열 형태인 경우 Rest Parameter + Generic 사용

function first<T>(data:[T,...unknown[]]){
  return data[0];
}
let num = first([1,2,3]);
let str = first([1,"a","b","c"]);

argument 값이 배열일 경우 T[ ]와 같은 형태로 작성해도 되지만 특정 index의 값만 필요한 경우 Rest Parameter(나머지 매개변수) 문법을 사용하여 표현이 가능하다.

 

응용.3 - Generic의 특정 Property 존재 여부에 따른 Extends 키워드 사용

function getLength<T extends {length: number}>(data:T){
  return data.length;
}
let one = getLength([1,2,3,4,5]);
let two = getLength("abcde");
let three = getLength({length: 10});
let four = getLength(50); //Error, 50 doesn't have 'length' property

Interface의 확장과 같은 형식의 문법으로 특정 속성을 포함하고 있는 Generic을 사용하여 표현이 가능하다.

[map, forEach method 타입 정의]

Map

- JS에서 많이 사용했던 배열 전용 method로 특정 배열을 map method를 통해 순회하며 그에 맞는 callback 함수를 입력해주면 callback 함수를 거쳐 새로운 배열을 반환해주는 method이다. 코드를 통해서 Generic 문법을 사용하여 map method와 동일한 기능을 하는 함수를 만들어보자.

//Javascript
const arr = [1,2,3];
const newArr = arr.map(e => e * 2);
console.log(newArr) //[2,4,6]
//Typescript
function map<T,M>(arr: T[],callback:(item: T) => M){
  let result = [];
  for(let i = 0; i < arr.length; i++){
    result.push(callback(arr[i]))
  }
}
map(arr,(i) => i * 2));
map(['a','b'], (i) => i.toUpperCase()); //typeof i === string
map(['a','b'], (i) => parseInt(i)); //typeof i === number

 

ForEach

- Map method와 비슷하게 배열을 순회하며 요소에 특정 작업을 하는 method이지만 map method와 다르게 forEach method는 새로운 배열을 반환하지 않고 기존 배열을 변화시킨다는 차이점이 있다. forEach method는 반환값이 없기 때문에 상대적으로 쉽게 함수로 표현이 가능하다.

//Javascript
const arr = [1,2,3];
arr.forEach(e => e * 2);
console.log(arr) // [2,4,6]
//Typescript
function forEach<T>(arr:T,callback:(item:T) => void){
  for(let i = 0; i < arr.length; i++){
    callback(arr[i])
  }
}
forEach(arr,(i) => console.log(i.toFixed()));
forEach(['a','b'], (i) => i);

 

[Generic Interface & Generic Type Alias]

Generic Interface

- Generic Interface란 말그대로 InterfaceGeneric 문법으로 사용하는 방식을 말한다. Generic Interface는 타입으로 사용할 때 반드시 타입 내부에 할당할 타입을 꺽쇠와 함께 표기를 해주어야 한다.

interface KeyValue<T,M> {
  key:T,
  value: M 
}
let keyvalue: KeyValue<string,number> = {
  key:"key",
  value: 1
}
let keyvalue1:KeyValue<string,number[]> = {
  key:"key",
  value: [1]
}

Generic에서 꺽쇠 내부에 있는 변수를 공식 문서에 따라 Type variable(타입 변수)라고 하는데 다른 이름으로는 Type Parameter,Generic Type Variable, Generic Type Parameter등 다양한 이름으로 불린다.

 

Index Signature + Generic Interface

이전에 알아보았던 Index Signature와 Generic Interface를 사용하여 문법 작성도 가능하다. Index Signaturepropertykey와 value의 규칙만 만족한다면 객체를 허용하는 유연한 객체 타입 정의 방식이다. Generic Interface와 함께 사용하는 예시 코드를 보자.

//Index Signature
interface A {
  [key:string]: number
}
let a:A = {
  key: 1
}
//Index Signature + Generic Interface
interface B<T> {
  [key:string]: T
}
let b:B<number> = {
  key: 10
}
let b1:B<boolean> = {
  key: true
}

 

Generic Type Alias

Type AliasGeneric을 이용해 작성하는 문법은 Interface를 활용하는 것과 매우 비슷하다.

type C<T> = {
  [key:string] : number
}
let c:C<number> = {
  key: 2
}

type D<M> = M;
let d:D<number> = 213;
let e:D<number> = 'abc'; //Error

 

Generic Interface의 활용

위와 같이 문법 구조에 대해서는 알아보았지만 어느 상황에 유용하게 쓰이는지 감이 오지 않을 수 있다. Generic Interface는 객체 타입들로 조합된 복잡한 객체 타입을 정의하여 사용할 때 코드를 깔끔하고 유연하게 사용할 수 있다. 사용 예시를 간단한 코드 예시를 보며 이해해보자.

interface Student {
  type: "student",
  school: string
}

interface Developer {
  type: "developer",
  skill: string
}
interface User<T> {
  name: string,
  profile: T
}
function goSchool(user:User<Student>){
  const school = user.profile.school;
  console.log(`Go To ${school}`);
}
goSchool(developerUser) //Error

const developerUser:User<Developer> = {
  name: "Tom",
  profile: {
    type: "developer",
    skill: "typescript"
  }
}
const studentUser:User<Student> = {
  name: "Bob",
  profile: {
    type: "student",
    school: "school"
  }
}

[Generic Class]

Generic Class

- Class를 선언할 때 Generic 문법을 사용하여 선언하여 활용성을 높이는 방식이 존재한다. 앞서 Generic에 대해 많이 살펴보았으니 코드만 보아도 어떤 식으로 동작하는지 쉽게 이해가 가능할 것이다.

class List<T> {
  constructor(private list:T[]){}
  push(data:T){
    this.list.push(data)
  }
  pop(){
    this.list.pop()
  }
  print(){
    console.log(this.list)
  }
}
const list = new List<number>([1,2,3]);
list.push(4);
list.pop();
list.print();

const list1 = new List<string>(['a','b','c']);
list.push('d');
list.pop();
list.print();

[Promise & Generic]

Promise & Generic

- Promise 객체는 JS에 내장되어 있는 객체로 비동기 통신에 사용하는 객체이다. Promisecallback 함수를 받으며 Promise가 성공적으로 반환되면 then method를 사용하여 처리하고 반환에 실패하면 catch method를 사용하여 통신 결과를 처리할 수 있는 객체이다. Promise는 비동기 작업의 결과값의 타입을 자동으로 추론할 수 없다. 따라서 반환을 성공적으로 받은 경우 실행하는 resolve 함수에는 비동기 통신의 결과값을 argument로 전달을 할 수 있는데 arguments 값의 타입을 Generic을 통해서 정의해줄 수 있다.

const promise = new Promise<number>((res,rej) => {
  setTimeout(() => {
    res(20);
  },2000);
});
promise.then(() => {
  res(20)
})
.catch(() => {
  console.log("error");
});

위의 코드에서 성공에 해당하는 res 함수의 argument 값의 타입을 Generic 타입을 통해 number type을 부여해주었다.

Promise는 Generic Class를 기반으로 타입이 선언되어 있기 때문에 비동기 처리의 결과값의 타입을 지정해줄 수는 있지만, 실패했을 경우에는 타입을 정의해줄 수 없다. 추가로 타입을 지정해주지 않았을 경우에는 자동적으로 Unknown Type으로 추론이 된다.

 

Promise를 반환하는 함수 예시

간단한 예시로 Promise를 반환하는 함수를 예시로 들어 반환값에 대한 타입을 정의해보는 코드를 작성해보자.

interface Post {
  id: number;
  title: string;
  content: string
}
//#1
function fetchPost():Promise<Post>{
  return new Promise((res,rej) => {
    setTimeout(() => {
      res({
        id: 1,
        title: "title",
        content: "content"
      })
    },2000)
  })
}
//#2
function fetchPost(){
  return new Promise<Post>((res,rej) => {
    setTimeout(() => {
      res({
        id: 1,
        title: "title",
        content: "content"
      })
    },2000)
  })
}
const poseReq = fetchPost();
poseReq.then((post) => {
  post.id
})

fetchPost 함수를 통해 Promise 객체를 반환 받으며 함수 처리가 성공했을 경우 post 객체의 id property를 가져오는 코드를 작성해보았다. 이 때 fetchPost 함수의 반환값에 대한 타입 처리를 해주지 않는 경우 자동적으로 반환값에는 Unknown Type이 추론되기 때문에 post.id로 접근을 하면 오류가 발생한다. 따라서 위와 같이 반환되는 Promise의 타입을 Generic을 통해서 정의해주어야 하는데 타입 정의 방식은 함수의 반환값에 정의를 하는 방법과 Promise에 직접 정의를 해주는 두가지 방법이 있다. 두가지 방법 모두 좋은 방법이지만 코드 작업 자체는 기본적으로 협업이 우선 시 되므로 첫번째 방법을 추천한다.

'Typescript' 카테고리의 다른 글

[TypeScript] - (9) 조건부 타입  (1) 2024.08.22
[TypeScript] - (8) Type 조작  (0) 2024.08.14
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31

[TypeScript Class]

Typescript Class(타입스크립트 클래스)

TS에서 Class를 생성하는 것은 JS에서와 큰 차이가 없다. 동일하게 Pascal Case로 Class 작명을 해준 후, Field를 작성하고, Constructor 키워드를 통해 생성자를 작성해주면 된다. 하지만 TS는 타입이 정의된 정적 타입 언어인만큼 Field와 Constructor의 arguments에도 타입을 정의해주어야 한다. 아래 코드로 JS와 TS로 각각 Class를 정의하는 방법을 확인해 보자.

//JavaScript
class Student {
  //field
  name;
  grade;
  age;
  //생성자
  constructor(name,grade,age){
    this.name = name;
    this.grade = grade;
    this.age = age;
  }
  study(){
    console.log("Study");
  }
}
const student = new Student("Tom","A+",10);

//TypeScript
class Student {
  //field
  name:string;
  grade:string;
  age:number;
  //생성자
  constructor(name:string,grade:string,age:number){
    this.name = name;
    this.grade = grade;
    this.age = age;
  }
  study(){
    console.log("Study");
  }
}
const student = new Student("Tom","A+",10);

FieldConstuctorarguments에 타입이 지정된 부분 말고는 차이점이 없는 것을 볼 수 있다.

또한 Class Inherit(클래스 상속)에 있어서도 큰 차이점이 없다. 마찬가지로 타입이 지정되는 부분에서만 차이점을 발견할 수 있다.

//Javascript
class Student {
  //field
  name;
  grade;
  age;
  //생성자
  constructor(name,grade,age){
    this.name = name;
    this.grade = grade;
    this.age = age;
  }
  study(){
    console.log("Study");
  }
  introduce(){
    console.log("hello!")
  }
}

//inherit
class StudentDev extends Student {
  //field
  favorite;
  //생성자
  constructor(name,grade,age,favorite){
    super(name,grade,age)
    this.favorite = favorite;
  }
  programming(){
    console.log(`${this.favorite}`)
  }
}
const student1 = new StudentDev("Bob","B",20,"Typescript");
//Typescript
class Employee {
  //field
  name:string;
  age:number;
  position:string;
  //생성자
  constructor(name:string, age:number,position:string){
    this.name = name;
    this.age = age;
    this.position = position;
  }
  work(){
    console.log("Work")
  }
}

class ExecutiveOfficer extends Employee {
  //field
  roomNum : number
  //생성자
  constructor(name:string,age:number,position:string,roomNum: number){
    super(name,age,position)
    this.roomNum = roomNum;
  }
}
const employeeA = new Employee("TOM",20,"dev");

JS에서 상속받는 것과 동일하게 Extends 키워드를 통해 Class를 상속 받고, 상속 받은 Class의 생성자 내부에서는 Super 키워드를 통해 상속받은 property들을 작성해주면 된다.

[Access Modifier(접근 제어자)]

Access Modifier(접근 제어자)

- Class의 Field에 접근에 대한 제어를 하는 역할의 키워드로 private, public, protected 3종류가 있다.

Public
- 따로 부여하지 않아도 기본값으로 설정되는 키워드로 Class의 내부, Class의 외부 모두에서 property에 접근을 할 수 있게 해준다.
Private
- 해당 Class 내부의 method에서만 본 키워드가 부여된 property에 접근이 가능하고 그 이외에는 어느 곳에서도 접근과 사용이 불가능하다.
Protected
- 해당 Class 내부와 상속받는 파생 Class에서만 접근이 가능하고 그 이외의 외부에서는 해당 property에 접근이 불가능하다.
class Employee {
  //field
  private name:string;
  protected age:number;
  public position:string;
  //생성자
  constructor(name:string, age:number,position:string){
    this.name = name;
    this.age = age;
    this.position = position;
  }
  work(){
    console.log(`${this.name}`)
  }
}
class ExecutiveOfficer extends Employee {
  //field
  roomNum : number
  //생성자
  constructor(name:string,age:number,position:string,roomNum: number){
    super(name,age,position)
    this.roomNum = roomNum;
  }
  func(){
    this.name; // Error
    this.age;
    this.position;
  }
}
const employeeA = new Employee("TOM",20,"dev");
employeeA.name = "Bob"; //Error
employeeA.age = 10;  //Error
employeeA.position = "V";

위의 코드에서 보면 name property private, age propertyprotected, position property public 제어자가 붙었다. 추가로 ExecutiveOfficer이라는 Employee의 파생 Class도 존재한다.

1. name property는 private이기 때문에 Employee Class 내부의 work method에서만 접근이 가능하고 파생 Class에서는 접근 시 오류가 발생하는 것을 볼 수 있다.
2. age property는 protected이기 때문에 파생 Class에서는 접근이 가능하다. 하지만 Class의 외부에서는 접근 시 오류가 발생하는 것을 볼 수 있다.
3. position property는 publice이기 때문에 특정 영역에 국한되지 않고 모든 곳에서 접근이 가능한 것을 볼 수 있다.

 

Access Modifier을 사용하는 방법에는 위와 같이 Field 값에 직접 부여하는 방법도 있지만 생성자에 직접 부여하는 방법도 존재한다. 이러한 경우 생성자가 자체적으로 Field를 만들기 때문에 Field에는 따로 제어자와 값을 정의하지 않아도 된다. 따라서 생성자에 제어자를 부여하는 경우 Field를 비워주어야 한다.

그에 더해 Field 값의 초기화까지 자동적으로 해주기 때문에 아래 코드와 같이 매우 간단한 코드로 작성이 가능하다.

class Employee {
  constructor(private name:string, protected age:number, public position:string){}
  work(){
    console.log("Work")
  }
}
접근 제어자는 객체 지향 프로그래밍에서 은닉화하는 경우에 굉장히 자주 사용된다.

[Interface & Class]

Interface & Class

InterfaceClass를 같이 사용하는 경우에는 InterfaceClass설계도, 청사진의 역할을 하게 된다. 따라서 Class를 생성할 때 Interface를 상속받는 느낌으로 코드를 구현하면 되는데 이 때 사용되는 키워드가 있다. 바로 Implements라는 키워드를 사용해서 Interface를 갖고 Class를 구현하게 되는데 Class를 상속받을 때 Extends 키워드를 통해 확장하여 상속을 받았듯이 Implements 키워드를 통해 Interface에 정의된 형태에 맞춰서 Class를 생성한다.

interface Character {
  name:string,
  speed: number,
  move():void
}

class CharacterA implements Character {
  name: string;
  speed: number;
  constructor(name:string,speed:number){
    this.name = name;
    this.speed = speed;
  }
  move():void {
    console.log("Move")
  }
}

여기서 주의해야할 점은 Interface를 통해서 생성되는 Field 값들은 무조건 Public 제어자를 갖게 된다. 따라서 Public이 아닌 다른 제어자를 사용하기 위해서는 Class의 Field나 생성자를 통해서 제어자를 별도로 부여해주어야 정상적으로 사용이 가능하다.

Interface와 Class는 자주 사용하게 되진 않지만 라이브러리의 구현이나 복잡하고 정교한 코드를 작성하는 경우에 유용하게 사용된다.

'Typescript' 카테고리의 다른 글

[TypeScript] - (8) Type 조작  (0) 2024.08.14
[TypeScript] - (7) Generic(제네릭)  (2) 2024.08.07
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31
[Typescript] - (3) Typescript 이해하기  (0) 2024.07.26

[Interface(인터페이스)]

Interface(인터페이스)

- 타입에 이름을 지어주는 또 다른 작명 문법이자 상호간에 약속된 문법이며 객체의 구조를 정의하는데 특화된 문법이다. Type Alias에는 없는 상속, 합침 등의 Type Alias에는 없는 특수 기능을 제공한다. 작성 방식은 Type Alias와 비슷한 형태로 작성한다. 하지만 정의하며 "="를 사용하지 않는다.

interface Person {
  name: string,
  age?: number,
  sayHi(): void,
  sayHi(a:number, b:number): void
}
const person:Person = {
  name: "Tom",
  age: 10,
  sayHi:() => {
    console.log("Hi")
  }
}
person.sayHi();
person.sayHi(1,2);

위의 코드와 같이 작성한다. InterfaceType Alias와 같이 타입을 정의하고, 객체의 method 타입도 정의 가능하며 Optional Property까지 적용이 가능하다. 

 

하지만 주의해야할 점이 몇가지 있다.

1. 객체 변수를 선언할 때 내부에 method가 존재하는 경우 Interface에 method의 타입을 정의할 때 Call Signature(호출 시그니쳐)를 통해 method의 타입을 정의하게 되면 선언한 객체가 함수 취급을 받기 때문에 오류가 발생한다. 따라서 method의 타입을 정의할 때에는 Call Signature 앞에 method의 이름을 적어주어야 한다.
interface Person {
  name: string,
  age?: number,
  (): void,		//X -> person is object
  sayHi(): void		// O
}
const person:Person = {
  name: "Tom",
  age: 10,
  sayHi:() => {
    console.log("Hi")
  }
}
2. InterfaceUnion Type이나 Intersection Type으로 정의가 되지 않는다. 독립적인 타입 정의 방식이기 때문에 다른 타입과 혼용이 불가능하다. 따라서 Interface를 정의해둔 후 해당 Interface와 다른 타입을 타입 주석으로 처리해주거나 별도의 Type Alias에 Union or Intersection 요소로 할당을 해서 사용해주어야 한다.
interface Person {
  name: string,
} | number //Error, interface can be a Union Type or Intersection Type

const person:Person | number = { //Not Error
  name: "Tom",
}
3. Interface의 이름을 정의할 때에는 대문자 "I"를 붙여서 이름을 짓는 경우가 있다고 한다. 하지만 JS를 주로 사용하던 개발자 입장에서는 Snake Case, Camel Case, Pascal Case 같이 다양한 방식으로 변수명을 입력했었는데 추가적인 작명 방식이 생겨나면 좋지 않기 때문에 어느 하나에 국한되지 않고 자신이 속한 회사나 그룹의 방식을 따라가는 것이 좋다.

 

[Extends Interface(인터페이스 확장)]

Extends Interface(인터페이스 확장)

- extends 키워드를 통해 다른 Interface로부터 해당 Interface가 갖고 있는 모든 property를 확장하여 사용할 수 있도록하는 기법이다. 사용 예시는 아래와 같다.

Interface Animal {
  name: string,
  age: number,
}
Interface Dog extends Animal {
  isBark: boolean
}
Interface Cat extends Animal {
  isScratch: boolean
}
const dog:Dog = {
  name: "Bob",
  age: 10,
  isBark: true
}
const cat:Cat = {
  name: "Bob",
  age: 10,
  isScratch: false
}

Interface 확장을 통해 property를 받아서 사용하는 경우를 Interface 상속이라고도 하는데, Type Alias에는 없지만 Interface에는 존재한다는 특별한 기능이다. 또한 Interface 상속을 받았다고해서 상속 받은 Interface의 속성의 타입을 변경할 수 없는 것은 아니다. 속성의 타입을 변경할 수 있지만 조건이 있다. 변경하려는 속성의 타입이 상속 받은 Interface의 속성의 타입의 Serve Type이어야 한다는 것이다. 글로 보면 이해하기 헷갈리니 코드를 통해 확인해보자.

interface A {
  name: string,
  age: number
}
interface B extends A { //Error
  name: number,
  age: number,
  isTrue: boolean
}
interface C extends A { //Not Error
  name: "Tom",
  age: number,
  isTrue: boolean
}

위의 코드 예시를 보면 interface B는 A로부터 상속을 받아 속성들을 사용하는데 name property의 타입을 number로 재정의 하였다. 이런 경우 두 타입의 계층이 같기 때문에 상속에 오류가 발생한다.

반면 interface C는 동일하게 A로부터 상속을 받고 name property의 타입을 String Literal Type으로 재정의 하였다. String Literal TypeString TypeServe Type이므로 오류가 발생하지 않는 것이다.

상속을 받았다는 것 자체가 한 쪽이 다른 한 쪽의 Serve Type으로 취급 되는 것이기에 내부 속성들도 규칙에 맞게 움직여야 하는 것이다.

Interface는 Type Alias로 정의된 타입도 상속이 가능하다. 객체 형태로 정의된 타입은 모두 확장이 가능, 상속 받는 것이 가능하다.

 

다중 확장

- 말그대로 확장을 다중으로 하는 것이다. 특정 Interface가 서로 다른 Interface들 모두를 상속받는 것을 의미한다. 어려운 내용은 아니기에 예시 코드만 작성하고 마무리하겠다.

interface A {
  name: string,
  age: number
}
interface B {
  isTrue: boolean,
  color: string
}
interface C extends A,B {
  isReal: boolean
}

const something:C = {
  name: "something",
  age: 10,
  isTrue: true,
  color: "red",
  isReal: "false"
}

 

[Declaration Merging(선언 합침)]

Declaration Merging(선언 합침)

- Type Alias와 달리 Interface는 중복 선언이 가능하며 중복 선언 시 모든 property들은 하나로 합쳐지게 된다. 동일한 Interface를 반복하여 정의하고 내부 속성이 다르다면 자체적으로 하나의 Interface로 합쳐지게 된다. 하지만 주의해야할 점이 있는데, 반복하여 Interface를 정의할 때 동일한 속성이 동일한 타입을 갖고 반복 정의되면 문제가 되지 않지만 동일한 속성이 다른 타입으로 정의되면 타입 충돌이 발생한다. 아래 코드를 예시로 보자.

interface A {
	name: string
}
interface A {
	age: number
}
interface A {
	name: number // Error, typeof name is "string"
}
interface A {
	name: "hello" // Error, typeof name is "string" not only another type but serve type too
}

const a: A = {
	name: "a",
	age: 1	
}

Interface A의 name property는 String Type으로 정의되어 있기 때문에 String Type이 아닌 다른 타입으로 정의가 되면 타입 충돌이 발생한다. Extends Interface(인터페이스 확장)에서는 Serve Type이라면 중복 선언이 가능했지만 선언 합침에 있어서는 불가능하다.

 

Declaration Merging(선언 합침)은 기본적인 코드보다 모듈 보강에 잘 사용된다고 한다. 외부 라이브러리의 모듈을 불러와 사용할 때 Interface가 이미 정해져있기 때문에 사용 과정에서 속성을 추가할 수가 없다. 이런 경우 추가하고자 하는 속성을 Interface에 넣기 위해 다시 선언을 하여 Interface가 합쳐지도록 하는 것이다. 자세한 과정을 모듈을 불러온 후 해야할 작업이 더 있기에 그 부분에 대해서는 나중에 다루어 보도록 하자.

interface Lib {
  name:string,
  age: number
}
interface Lib {
  color: string
}
const lib:Lib = {
  name: "a",
  age: 10,
  color: "black"
}

 

참고 : Inflearn - 한 입 크기로 잘라먹는 Typescript(이정환)


[Interface]

TS 내에서 Class나 object의 형태를 지정해주는 방법은 두가지가 있다. Type 키워드를 사용해서 형태와 타입을 지정해주는 방식과 Interface를 사용하는 방식 두가지가 있다. 두가지 방법은 Abstract Class를 대체하는 방식이기도 한데, Abstract Class는 TS에서 JS로 컴파일 되는 과정에서 사라지지 않고 일반 Class로 남아있지만 Interface와 Type은 컴파일 과정에서 사라지기 때문에 코드를 경량화 시킬 수 있고 문법적으로 더 편하게 사용할 수 있다. 하지만 두 가지 방법 중에서 Interface를 사용하는 것이 object와 class에 한해서 더 편리하고 유용하다.

1. 첫번째로 확장과 상속에 있어서 Interface가 편리하고 쉽다.

type은 확장을 하기 위해서는 새로운 타입을 정의하면서 기존의 타입을 부여하면서 AND 연산자를 통해서 확장할 속성을 별도로 입력해주어야 한다. 하지만 Interface는 Extends 키워드 하나만으로 간단하게 타입 확장이 가능하다.

//Type
type A = {
  name: string
}
type B = A & {
  age: number
}
const a = getA();
a.name;
a.age;
//Interface
interface A {
  name: string
}
interface B extends A {
  age: number
}
const b = getB();
b.name;
b,age;

 

2. 두번째로 새로운 field를 추가할 때 Interface의 Declaration Merging(선언 합침)이라는 방식으로 간단하게 새로운 filed를 추가할 수 있다. type은 한 번 정의한 type을 다시 정의할 수 없다. 하지만 Interface는 제한 없이 중복적으로 정의하여도 TS 자체적으로 하나로 합쳐버리는 선언 합침이 존재하기 때문에 중복 정의가 가능하다.

//Type
type A = {
  name: string
}
type A = {		//Error: Duplicate identifier "A"
  age: number
}
//Interface
interface B {
  name: string
}
interface B {
  age: number
}
const b: B = {
  name: "B",
  age: 10
}

 

 

참고 : Nomad Coders - 타입스크립트로 블록체인 만들기

'Typescript' 카테고리의 다른 글

[TypeScript] - (7) Generic(제네릭)  (2) 2024.08.07
[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (4) Typescript 함수  (0) 2024.07.31
[Typescript] - (3) Typescript 이해하기  (0) 2024.07.26
[TypeScript] - (2) Typescript 기본  (3) 2024.07.23

[함수 타입]

함수의 타입

일반적으로 함수를 설명할 때 JS에서는 매개변수가 무엇이고 결과값이 무엇인지에 대한 정보가 중요하다.

하지만 TS에서는 매개변수와 결과값도 중요하지만 각 값들의 타입이 무엇인지에 대한 정보도 중요하다.

//함수 선언문
function func(a:number,b:number) : number {
  return a + b;
}
//화살표 함수
const add = (a:number, b:number) : number => a + b;

위의 코드와 같은 방식으로 매개변수와 결과값의 타입을 지정해주는데, return문이나 "=>"가  있는 경우 매개변수의 타입을 보고 TS 자체적으로 결과값의 타입을 추론하기도 한다.

 

함수의 매개변수

매개변수의 타입을 정의해줄 경우 기본값을 설정해둘 수 있다. 하지만 기본값을 기준으로 추론된 타입과 그 매개변수의 타입을 다르게 정의하면 오류가 발생한다. 더해서, 매개변수로 정의되어 있지 않은 타입을 인수로 받아 함수를 실행하면 역시나 오류가 발생한다.

function func(name="Tom"){
  console.log(`name: ${name}`)
}
func("Bob");
func(10); //Error

function func(name:number="Tom"){   //Error
  console.log(`name: ${name}`)
}

매개변수로 정의는 해두었지만 함수를 실행할 때 필수로 넣어야하는 매개변수가 아니라면 Optional Parameter(선택적 매개변수)를 사용하면 된다. 앞서 보았던 Optional Property(선택적 프로퍼티)와 같이 JS의 Optional Chaining 방식으로 적용되는 문법이며 선택적 매개변수로 정의된 매개변수는 타입 추론시 Union Type으로 추론된다. 따라서 해당 매개변수를 함수 내에서 사용할 때에는 Type Guard를 이용해서 조건으로 처리를 해주어야 한다.

function func(name="tom", age:number){
  console.log(`name: ${name}`);
  console.log(`age: ${age}`);  
}
func("bob",10);
func("bob"); //Error, There are two parameters

function func(name="tom", age?:number){
  console.log(`name: ${name}`);
  console.log(`age: ${age - 5}`);  //Error, typeof age can be undefined
}

function func(name="tom", age?:number){
  console.log(`name: ${name}`);
  if(typeof age === "number"){
    console.log(`age: ${age - 5}`);
  }
}
Optional Parameter(선택적 매개변수)는 반드시 Essential Parameter(필수 매개변수)보다 뒤에 위치해야한다.

 

Rest Parameter(나머지 매개변수)

가변적인 길이의 인수들을 배열로 묶어서 rest 변수에 할당하여 전달하는 JS내의 문법이다. Rest Parameter로 매개변수를 받을 시에는 배열 형태로 받으면 된다. 함수 실행 시 받을 인수의 타입에 맞게 배열 타입을 정의해주면 나머지 매개변수로 받을 수 있다. 또한 Tuple Type을 통해 받을 매개변수의 개수를 설정할 수도 있다. Rest Parameter 자체가 배열이기 때문에 Tuple Type으로 정의가 가능한 것이다.

function func(...rest: number[]){
  let sum = 0;
  rest.forEach(e => sum += it);
  return sum;
}
func(1,2,3,4,5); //15
func(1,2,3); //6
------------------------------------------------
//Rest Parameter + Tuple
function func(...rest: [number,number,number]){
  let sum = 0;
  rest.forEach(e => sum += it);
  return sum;
}
func(1,2,3,4,5); //Error
func(1,2,3); //6

 

[함수 타입 표현식과 호출 시그니쳐]

함수 타입 표현식(Function Type Expression)

앞에서 살펴보았던 Type Alias(타입 별칭) 기법을 함수에서 사용하는 방식이다. 함수 내에 타입을 여럿 정의하면 코드의 길이도 길어질뿐만 아니라 서로 다른 함수가 같은 타입의 매개변수와 반환값을 갖는 경우 범용적으로 사용이 가능해서 유용한 방식이다. 사용 방식은 아래 코드와 같다.

//1. Type Alias
type Operation = (a:number, b: number) => number;
const add:Operation = (a,b) => a + b;
const sub:Operation = (a,b) => a - b;
const multi:Operation = (a,b) => a * b;
const divide:Operation = (a,b) => a / b;
//2. Literal Type
const add:(a:number, b: number) => number = (a,b) => a + b;
const sub:(a:number, b: number) => numbern = (a,b) => a - b;
const multi:(a:number, b: number) => number = (a,b) => a * b;
const divide:(a:number, b: number) => number = (a,b) => a / b;

함수 타입 표현식을 사용하게 되면 매개변수로 정의된 타입 및 개수에 맞춰서 함수에 인수로 담고 실행해야 한다는 점을 주의해야한다.

 

호출 시그니쳐(Call Signiture)

함수 타입 표현식과 마찬가지로 Type Alias 기법을 사용한 방식으로 함수판 Type Alias라고 보면 된다. JS 문법에서 다루어 보았듯이 함수는 객체의 일종이기 때문에 객체 형태로 타입을 지정하는 방식이다. 타입 정의 방식부터 코드로 보자.

type Operaion = {
  (a:number, b:number) : number
}
const add: Operation2 = (a,b) => a + b;
const sub: Operation2 = (a,b) => a - b;

또한 하이브리드 타입이라고 해서 호출 시그니쳐 기법으로 정의된 타입에 프로퍼티를 추가로 입력하여 활용하는 타입이 있다. 사용에 추천은 하지 않지만 간단하게 형태만 알아보도록 하자.

type Operaion = {
  (a:number,b:number) : number;
  name: string
}
const add:Operation = (a,b) => a + b;
add(1,2);
add.name = "Tom" //Not Error

 

[함수 타입의 호환성]

함수 타입 호환성

- 특정 함수 타입을 다른 함수 타입으로 취급해도 되는지 여부를 판단하는 것이다. 서로 다른 타입의 함수가 있을 때 함수 간의 호환성을 따져본다고 볼 수 있다. 호환성 확인에는 크게 두가지로 나뉜다.

 

1. 반환값의 타입 호환성

- 반환값의 타입 호환성은 반환값이 Upcasting 되는 경우 호환된다.

type A = () => number; //number Type
type B = () => 10;  //number literal type

let a:A = () => 10;
let b:B = () => 10;
a = b;
b = a; //Error

위의 코드에서 보면 Number Type은 Number Literal Type의 Super Type이기 때문에 변수 b를 변수 a로 대입하는 것은 Upcasting이라 가능하지만 그 반대는 Downcasting이기 때문에 불가능하다.

 

2. 매개변수의 타입 호환성

 

2-1. 매개변수의 개수가 같은 경우

- 매개변수가 Downcasting 되는 경우 호환된다.

type C = (value:number) => void; //parameter -> Number Type
type D = (value:10) => void; // parameter -> Number Literal Type

let c:C = (value) => {};
let d:D = (value) => {};
c = d; //Error
d = c;
---------------------------------------------
//Object
type Animal = {
  name: string
}
type Dog = {
  name: string,
  color: string
}

let animalFunc = (animal:Animal) => {
  console.log(animal.name);
}

let dogFunc = (dog:Dog) => {
  console.log(dog.name);
  console.log(dog.color);
}
animalFunc = dogFunc; //Error
dogFunc = animalFunc;

let testFunc = (animal:Animal) => {
  console.log(animal.name);
  console.log(animal.color); //Error, Animal Type doesn't have color property
}

let testFunc1 = (dog:Dog) => {
  console.log(dog.name);
}

위의 코드를 보았을 때 함수의 경우 변수 c의 매개변수는 Number Type, 변수 d의 매개변수는 Number Literal Type이다. 따라서 변수 d를 변수 c에 대입하는 경우 Upcasting이 되기 때문에 오류가 발생하고, 변수 c를 변수 d에 대입하는 경우 Downcasting이 되기 때문에 호환이 가능한 것이다.

그 아래 객체 형태의 Type Alias와 그에 맞게 생성된 함수를 보면 Animal 타입은 Dog 타입보다 갖고 있는 속성의 개수가 적기 때문에 Animal 타입은 Dog 타입의 슈퍼 타입이 된다. 우리가 이해하고 있던 타입 호환성에 따르면 Dog 타입으로 정의된 dogFunc를 Animal 타입으로 정의된 animalFunc에 대입하였을 때 Upcasting이라 가능할 것 같지만 함수의 매개변수 호환성에서는 반대로 Downcasting이 되어야 호환이 된다.

 

2-2. 매개변수의 개수가 다른 경우

- 매개변수의 개수가 더 적은 쪽이 더 많은 쪽으로 호환 가능하다.

type Func1 = (a: string,b:number) => void;
type Func2 = (a: string) => void;

let func1:Func1 = (a,b) => {};
let func2:Func2 = (a) => {};

func1 = func2;
func2 = func1; //Error
------------------------------------------
type Func1 = (a: number,b:number) => void;
type Func2 = (a: string) => void;

let func1:Func1 = (a,b) => {};
let func2:Func2 = (a) => {};

func1 = func2; //Error
func2 = func1; //Error

위의 코드를 보면 Func1 타입으로 정의된 func1의 매개변수는 2개, Func2 타입으로 정의된 func2의 매개변수는 1개이다. 간단하게 생각해서 매개변수의 개수가 더 적다면 호환이 되지 않는다. 따라서 func2에 func1을 대입하려고 하면 매개변수의 개수가 더 적어지게 되므로 오류가 발생하는 것이다. 

여기서 주의해야할 점은 개수가 다르더라도 호환이 되기 위해서는 같은 매개변수의 타입은 동일해야 한다는 점이다. 애초에 매개변수의 타입이 모두 다르다면 호환 자체가 불가능해진다.

[함수 오버로딩]

함수 오버로딩

- 함수를 매개변수의 개수나 타입에 따라 여러가지 버전으로 정의하는 방법이다. 함수의 다양한 버전을 정의하는 Overoad Signature(오버로드 시그니쳐)가 있고 실제로 구현을 하는 Implement signature(구현 시그니쳐)가 있다. 코드를 작성해보며 살펴보자.

//Overoad signature
function func(a:number):void;
function func(a:number, b:number, c:number):void;

//Implement Signature
function func(a:number,b?:number,c?:number){
  if(typeof b === "number" && typeof c === "number"){
    console.log(a+b+c);
  }else{
    console.log(a*20);
  }
}

위의 코드를 보면 매개변수가 1개일 경우에는 20을 곱해주고 3개인 경우에는 모든 매개변수의 합을 반환하는 함수를 작성하였다. 매개변수의 개수에 따른 버전을 나누어두고 함수를 실행하는 것이다. 매개변수의 타입에 따라서도 버전을 나누어 사용할 수 있다. 함수 오버로딩은 다양한 라이브러리를 사용할 때 유용하다.

[사용자 정의 타입 가드]

사용자 정의 타입 가드(Custom Type Guard)

- 말그대로 사용자의 입맛에 맞게 타입 가드를 설정하는 기법이다. 간단한 코드를 보면서 이해하는 것이 쉬울 것 같다.

type Dog = {
  name: string,
  isBark: boolean
}
type Cat = {
  name: string,
  isScratch: boolean
}
type Animal = Dog | Cat;

function warning(animal:Animal){
  if("isBark" in animal){
    animal;
  }else if("isScratch" in animal){
    animal;
  }
}

위의 코드는 Type Alias로 정의된 타입의 property를 갖고 Type Guard를 생성하여 타입 좁히기를 통해 특정 기능을 수행하는 과정이다. 하지만 앞에서 공부했듯이 특정 속성명을 갖고 타입 좁히기를 하면 코드의 직관성이 떨어지고 해당 속성이 어느 Type의 속성인지 찾아봐야하는 번거로움도 발생한다. 또한 외부 라이브러리를 사용하는 경우에는 속성명이 변경되기라도 하면 타입 좁히기에 오류가 발생할 가능성이 높아진다. 이 부분을 방지하기 위해 사용자가 직접 Type Guard를 만들어서 사용하는 것이다.

type Dog = {
  name: string,
  isBark: boolean
}
type Cat = {
  name: string,
  isScratch: boolean
}
type Animal = Dog | Cat;

function isDog(animal:Animal): animal is Dog{
  return (animal as Dog).isBark !== undefined
}

function isCat(animal:Animal) : animal is Cat{
  return (animal as Cat).isScratch !== undefined
}

function warning(animal:Animal){
  if(isDog(animal)){
    animal;
  }else if("isScratch" in animal){
    animal;
  }
}

isDogisCat이라는 함수를 통해서 Type Guard를 생성하였다. 각 함수는 매개변수의 타입이 Union Type으로 정의된 Animal Type을 받고 있다. 하지만 TS에서는 사용자가 직접 만든 함수의 반환값을 갖고는 타입 좁히기가 실행되지 않는다. 따라서 반환값의 타입을 정의해주면서 Custom Type Guard를 생성하게 되는것이다. "animal is Dog/Cat"으로 반환값의 타입을 Type Alias로 정의해둔 타입이라고 정의해주면 warning 함수 내부의 조건문에서 자동으로 타입 좁히기를 실행하여 원하는 타입으로 사용이 가능하게 된다.

 

'Typescript' 카테고리의 다른 글

[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (3) Typescript 이해하기  (0) 2024.07.26
[TypeScript] - (2) Typescript 기본  (3) 2024.07.23
[Typescript] - (1) Typescript 사용하기  (2) 2024.07.19

[타입스크립트의 이해]

타입을 정의하는 기준, 타입간의 관계를 정의하는 기준, 타입의 오류를 검사하는 기준 등 타입스크립트의 구체적인 원리와 동작 방식을 이해한다.


[타입은 집합이다]

타입은 집합으로 이해를 하면 쉽다. 타입은 값들을 포함하고 있는 집합이며 계층도로 표현이 가능하다.

 

타입 계층도


슈퍼타입(부모 타입) : 상대적으로 더 큰 범주의 타입 및 집합

서브타입(자식 타입) : 상대적으로 더 작은 범주의 타입 및 집합

//number type은 number literal type의 슈퍼 타입이다.
const a:number = 1; //number type
const b:10 = 10; // number literal type

 

 

타입 호환성

타입 호환성 : 어떤 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것. 일반적으로 업캐스팅은 대부분 가능하지만 다운 캐스팅은 거의 불가능하다.

const a:number = 1; //number type
const b:10 = 10; // number literal type
/**
*a와 b가 모두 type 자체는 숫자 형태라고 하더라도 b는 10이라는 숫자 하나만을 type으로 명시하고 있고
*a는 number 전체 범주를 명시하고 있기 때문에 a를 b라고 보기엔 어려움이 있다.
*/

업 캐스팅 : 서브 타입을 슈퍼 타입으로 취급, 모든 상황에 가능

다운 캐스팅 : 슈퍼 타입을 서브 타입으로 취급, 대부분 불가능

unknown type은 모든 type의 슈퍼 타입이며, never type은 모든 타입의 서브 타입이다. 쉽게 코드로 보면 다음과 같다.

//Unknown type
function unknownExam(){
  let a:unknown = 1; 		//Upcasting
  let b:unknown = 'a';		//Upcasting
  let c:unknown = true;		//Upcasting
  let d:unknown = null;		//Upcasting
  let e:unknown = undefined;	//Upcasting
  let unknownVar: unknown;

  let num: number = unknownVar; //Error
  let str:string = unknownVar;	//Error
  let bool:boolean = unknownVar;//Error	
}

//Never type
function neverExam(){
  function neverFunc():never {
    while(true){}
  }
  let num:number = neverFunc();	//Upcasting
  let str:string = neverFunc();	//Upcasting
  let bool:boolean = neverFunc();//Upcasting

  let never1:never = 10; //Error
}

//Void type
function voidExam(){
  function voidFunc():void {
    console.log("hi");
  }
  let voidVar: void = undefined; //void type은 undefined type의 슈퍼타입
}

//any type
function anyExam(){
  let unknownVar:unknown;
  let anyVar: any;
  let undefinedVar:undefined;
  let neverVar:never;
  //any type은 모든 type의 슈퍼 타입도 가능하고 서브 타입도 가능하다.
  anyVar = unknownVar;
  anyVar = undefinedVar;
  undefinedVar = anyVar;
  
  anyVar = neverVar; //Error
  neverVar = anyVar; //Error
  //any type은 모든 type 간의 관계에서 자유롭지만 never type한테만큼은 자유롭지 못하다.
}

[객체 타입의 호환성]

객체 타입의 호환성

특정 객체 타입을 다른 객체 타입으로 취급해도 괜찮은지에 대한 호환성을 따지는 것이다.

앞에서 말했던 슈퍼 타입과 서브 타입 간의 관계를 보면 서브 타입이 더 작은 범주라는 것을 알 수 있다.

위의 예시를 다시 보면

const a:number = 10;
const b:10 = 10;

위의 예시에서 a는 number type이라는 더 큰 범주를 갖고 있고 b는 number literal type으로 10이라는 값 하나만 갖고 있기 때문에 a가 b의 슈퍼 타입이라는 것을 알 수 있다.

 

하지만 객체에서의 호환성을 보면 헷갈리게 되는데 객체로 예시를 들어보면

type Animal = {
  name: string,
  age: number
}
type Dog = {
  name: string,
  age: number,
  breed: string
}
//super type
let animal: Animal = {
  name: 'Tom',
  age: 10
}
//serve type 
let dog: Dog = {
  name: 'mike',
  age: 5,
  breed: 'mal'
}
animal = dog; // up casting
dog = animal; //Error, down casting

Type Alias로 작성된 Animal type과 Dog type을 비교했을 때 Dog type이 더 많은 프로퍼티를 갖고 있기 때문에 더 큰 범주가 아니냐고 헷갈릴 수 있지만 TS에서는 누구나 갖고 있는 프로퍼티를 갖고 있는 type이 슈퍼 타입이 된다.

따라서 아래의 animal과 dog 변수로 UpCasting과 DownCasting을 하였을 때 DownCasting에서 에러가 난 것으로 증명되며 객체 타입의 호환성에 대해 알 수 있다.

 

초과 프로퍼티 검사

객체 타입의 변수를 초기화 할 때, Type Alias로 정의해두지 않은 프로퍼티를 추가로 입력하였을 때 자동적으로 오류를 발생시키는 검사이다. 말로만 들으면 어려우니 간단한 코드로 보면

type Book = {
  name: string,
  price: number
}
let JSBook: Book = {
  name: "This is JSBook",
  price: 20000,
  skills: "Javscript" //Error
}

Type Alias로 정의해둔 Book 타입에는 skills라는 프로퍼티가 존재하지 않지만 새로 초기화를 하는 JSBook이라는 변수에는 skills라는 프로퍼티가 추가되어있다. 이러한 경우 기존에 정의되어 있지 않는 프로퍼티를 초과하여 입력하였기 때문에 TS 자체적으로 초과 프로퍼티 검사를 통해서 에러를 발생시킨다.

초과 프로퍼티 검사의 에러를 방지하기 위해서는 아래 코드와 같이 객체 리터럴로 변수를 초기화하지 않고 추가하고자하는 프로퍼티가 포함되어있는 타입을 새로 정의한 후 할당하는 것이 좋다.

//Type Alias
type Book = {
  name: string;
  price: number;
}
type NewBook = {
  name: string;
  price: number;
  skills: string
}
//초과 프로퍼티 검사를 피하기 위한 변수 초기화
let codeBook:NewBook = {
  name: "Typescript",
  price: 20000,
  skills: "typescript"
}
//객체 리터럴 타입이 아닌 초기화 되어있는 변수를 할당
let book1: Book = codeBook;

[대수 타입]

대수 타입이란?

여러 개의 타입을 합성해서 새롭게 만들어 낸 타입, Union TypeIntersection Type이 있다.

 

Union Type(합집합 타입)

- 우리가 흔히 알고 있는 합집합이라고 생각하면 쉽다. 서로 다른 두 타입 모두를 허용하는 타입을 말한다. 코드로 보면 이해가 더 쉽기 때문에 아래에 예시를 보자.

let a:number | string | boolean;
a = 10; //Not Error
a = "hello" //Not Error
a = true //Not Error

let arr: (number | string | boolean)[] = [1,"hello",false]; //Not Error

//객체 타입
type User = {
  name: string;
  age: number
}
type Lang = {
  name: string;
  lan: string;
}
type Union = User | Lang;

let person: Union = {
  name:"Tom",
  age: 10
}
let person1: Union = {
  name:"Bob",
  lan: "kor"
}
let person2: Union = {
  name:"Mike",
  age: 20,
  lan: "en"
}
변수 a와 arr에 Union Type이 정의되었기 때문에 정의된 타입의 자료형이라면 어떤 변수를 할당해도 오류가 발생하지 않는다. 또한 객체 형태의 Union으로 정의된 타입은 User 타입과 Lang 타입의 합집합 개념으로 변수 person2와 같이 두 타입에 정의되어있는 속성을 모두 사용해도 오류가 발생하지 않는 것이다.

 

Intersection Type(교집합 타입)

- 쉽게 말해 교집합 타입이다. 교집합의 개념과 같에 두 타입 모두의 서브 타입을 의미한다고 생각해도 된다. 하지만 기본 타입끼리의 Intersection Type은 대체로 Never Type으로 추론이 될 것이다. 기본 타입 간에는 겹치는 부분이 없기 때문이다.

let a: number & string; //typeof a === never
//number type과 string type의 공통 서브 타입은 never type뿐이다.

 

따라서 Intersection Type은 일반적으로 객체 자료형에서 자주 사용된다. 다른 속성을 가진 두 타입이 정의되었을 경우 특정 변수가 두 타입의 속성 모두를 갖고 있다면 두 타입 모두의 서브 타입이 된다. 쉽게 말해 두 집합(타입)의 교집합(Intersection Type)이 되는 것이다.

type Dog = {
  name: string;
  age: number
}
type Person = {
  name: string;
  language: string;
}
type Intersection = Dog & Person;

let intersection: Intersection = {
  name: "Tom",
  age: 10,
  language: "kor"
}
let intersection1: Intersection = {
  name: "Tom",
  age: 10,
} // Error

[타입 추론]

타입 추론

- 타입을 정의하지 않은 특정 변수에 타입을 추론할만한 근거가 있다면 TS 자체적으로 타입을 추론하는 방식. JS와 같이 키워드, 변수명, 초기값만을 적었을 때 TS 자체적으로 해당 변수의 타입을 추론하게 된다. 다만 할당된 값에 의해서 추론을 하기 때문에 추론을 할 수 있는 근거가 있어야 추론이 가능해진다.

let a = 10;
let b = "hello";
let c = {
  name:"tom",
  age: 10
}
/**타입 추론
* a : number
* b : string
* c : { name: string; age:number }
*/

타입 추론은 TS 자체적으로 타입 넓히기라는 방식으로 진행된다. 변수에 할당된 값을 보고 범위를 넓혀서 타입을 추론하는 방법이다.

 

Any Type의 진화(암묵적 Any Type)

- 변수에 명시적으로 Any Type을 정의하지 않고 초기화하지 않았을 경우 자동으로 Any Type으로 추론이 된다. 이 때 특정 값을 할당해줄 때마다 할당 받은 값에 따라 타입을 추론하여 자동으로 변수의 타입이 정의되고 변하는데 이것을 Any Type의 진화, 암묵적 Any Type이라고 한다.

let a;
a = 10; // typeof a === number
a.tofixed();
a = "hello"; // typeof a === string
a.toUpperCase();
a.toFixed(); //Error

하지만 이 방식을 추천하지 않는다. 오류 발생의 확률이 높으며 협업 시 문제가 되기 때문이다.

 

Const 키워드로 선언된 상수의 타입

- JS에서 공부해서 알듯이 const 키워드로 선언된 변수 즉, 상수는 값이 변하지 않고 값을 재할당 할 수 없다. 따라서 const로 선언된 변수는 Literal Type으로 여겨진다.


[타입 단언(Type Assertion)]

타입 단언

- "변수의 값 as 단언"의 형태로 타입의 추론을 위해서 타입을 단언해주는 것을 말한다. 주로 객체 타입에서 사용되는데 이전에 알아보았듯이 객체 리터럴 타입이 아닌 object나 "{ }(중괄호)"로 타입을 정해주면 내부 속성에는 접근할 수 없다. 이러한 경우 Type Alias로 정의해둔 타입과 속성값이 있다면 as를 사용하여 타입을 단언해줄 수 있다. 아래 예시를 보자.

type User = {
  name: string,
  age: number
}
let user = {}; //Error
user.name = "Tom"; // Error
user.age = 10; //Error

//Type Assertion
let user = {} as User;
let user = <User>{}; //Can't use in .tsx file
user.name = "Tom"; // Not Error
user.age = 10; // Not Error

위의 예시 코드와 같이 as를 사용해서 타입 단언을 해주면 기존에 알고 있던 오류가 발생하지 않는다.

타입 단언은 변수의 타입이 정의되어있지 않은 경우 TS 컴파일러에게 "해당 변수는 as로 단언해주는 타입을 가진 것이니 에러를 발생시키지 말아라"라고 명령하는 것 같은 개념이다. 컴파일러를 눈속임하는 느낌이다.

하지만 위 코드처럼 타입 단언을 한 뒤 속성과 속성값을 변수를 빈 객체로 초기화 한 후 입력해주지 않고 빈 객체로 두면 역시나 오류가 발생한다. TS에서 특정 타입이라고 단언을 해놓고 왜 아무 데이터도 존재하지 않는가에 의구심을 갖고 에러를 발생시키는 것이다. 하지만 이 오류는 Syntax Error(문법적 오류)가 아닌 Runtime Error(런타임 오류)로 컴파일 단계를 건너뛰고 에러가 발생하기 때문에 TS의 장점을 활용하지 못한 좋지 못한 케이스가 된다. 이런 이유로 인해 타입 단언을 가능한 사용하지 말라고들 권한다.

 

타입 단언은 앞서 살펴본 초과 프로퍼티 검사도 무난히 통과가 된다.

type Man = {
  name: string,
  age: number,
}
let human = {
  name: "Tom",
  age: 10,
  language: "kor"
} as Man; //Type Assertion, Not Error
let human = {
  name: "Tom",
  age: 10,
  language: "kor" //Error, Exceed Property
}

 

타입 단언의 규칙

- "변수의 값 as 단언"의 단언 형태에서 변수의 값과 단언의 관계가 한 쪽의 슈퍼 타입이거나 서브 타입이어야 한다. 타입 간의 관계를 잘 알고 있어야 타입 단언의 규칙을 어기지 않을 수 있다.

let a = 10 as never; //never type is serve type of number type
let b = 10 as unknown; //unknown type is super type of number type
let c = 10 as string; //Error, string type is not serve,super type of number type

 

다중 단언

- unknown은 모든 타입의 슈퍼 타입이므로 타입 단언을 한 과정 거쳐서 만능으로 바뀌는 과정을 말한다.

let a = 10 as unknown as string;
/**unknown type is super type of all of type. Therefore, type asserting from number type 
* to string type don't cause error
*/

하지만 Any Type과 같은 치트키 느낌이므로 피치 못할 사정이 아니면 사용하지 않는 것이 좋다.

 

const 단언

- 객체 내 모든 속성 값을 readonly 속성으로 전환한다. 특정 객체의 타입을 const로 단언을 하면 해당 객체의 속성들은 모두 읽기 전용 속성으로 전환된다.

let man = {
  name: "Tom",
  age: 10,
  lan: "en"
} as const
man.age = 20; //Error, man.age property is readonly
man.lan = "kor"; //Error, man.lan property is readonly

 

Non Null 단언

- Optional Chaining 방식의 보완 방식이라고 보면 쉽다. Type Alias의 정의에서 Optional Property를 정의하였을 때 객체의 해당 속성에 접근을 하기 위해서는 JS의 문법 Optional Chaining을 사용해서 접근하는데, TS에서 Optional Property정의된 타입이거나 undefined,null 타입일 수도 있다고 예상하기 때문에 아래의 예시 코드에서 len 변수의 타입이 number로 정의되었기 때문에 오류가 발생하는 것이다. 따라서 Null이 아니라는 뜻을 가진 Non Null 연산자인 "!"를 붙여주어 Non Null 단언을 통해 오류 발생을 막아줄 수 있다.

type Post = {
  title: string,
  author?: string
}
let post:Post = {
  title: "Hello",
  author: "Bob"
}
const len:number = post.author?.length; //Error, post.author.length can be a undefined type
const len:number = post.author!.length; //Not Error by Non Null

[타입 좁히기]

타입 좁히기

- 조건문 등을 이용해 넓은 타입에서 좁은 타입으로 타입을 상황에 따라 좁히는 방법. 임의의 함수에서 매개 변수의 타입이 여러 가지이고, 그 매개 변수를 통해 다양한 조건 처리를 할 때 유용하다. 타입을 좁히기 위해 사용하는 조건문,조건을 흔히 Guard라고 부른다.

조건문의 조건에 typeof 연산자를 사용하여 변수의 타입에 대한 조건을 설정하면 Union Type으로 정의된 변수의 타입의 범위가 좁혀져 특정 타입으로 여겨지게 된다.

function func(value: number | string){
  if(typeof value === "number"){
    console.log(value.toFixed()); //type of value === number
  }else if(typeof value === "string"){
    console.log(value.toUpperCase()); //type of value === string
  }
}

Union Type으로 정의된 변수의 타입에 객체가 들어가는 경우는 어떨까

function func(value: null | Date){
  if(typeof value === "object"){
    console.log(value.getTime()); //Error, null type is also object type in JS
  }
  if(value instanceof Date){
    console.log(value.getTime()); //Not Error
  }
}

JS에 내장 되어있는 class 객체의 경우에는 매개 변수의 타입을 instanceof  연산자를 통해 처리한다. instanceof 연산자의 뒤에는 타입이 들어가서는 안되며 객체가 들어가야 한다.

 

타입 별칭을 통해 임의로 생성한 객체 type의 경우에는 매개 변수 타입을 && 연산자와 in 연산자를 합해서 사용한다.

type Person = {
  name: string,
  age: number
}
function func(value: Person){
  if(value instaceof Person){
    console.log(value.name); //Error, Person is type, not a class object
  }
  if(value && "age" in value){
    console.log(value.age); //Not Error
  }
}

in 연산자를 통해서 변수 내부에 특정 속성이 있는지 여부를 확인하였지만 변수의 값이 undefined이거나 null이면 안되기 때문에 && 연산자를 통해서 undefined,null 값인 경우를 배제하여 조건 처리를 하는 것이다.


[서로소 유니온 타입]

서로소 유니온 타입

- 교집합이 없는 타입들로만 만든 유니온 타입을 말한다. 같은 속성을 가진 객체 타입들이 존재할 때 각 타입별로 공통된 부분을 없애서 데이터 처리를 하기에 유용한 타입이다. 코드를 직접 보는 것이 가장 이해가 쉬울 것 같다.

type Admin ={
  name: string,
  kickCount: number
}
type Member = {
  name: string,
  point: number
}
type Guest = {
  name: string,
  visitCount: number;
}
type User = Admin | Member | Guest;

function func(user:User){
  if('kickCount' in user){
    //Admin
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
  }else if('point' in user){
    //Member
    console.log(`${user.name}님 현재까지 ${user.point}모았습니다.`);
  }else{
    //Guset
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
  }
}
위의 코드에서 func 함수를 보았을 때 내부에 조건문들에 in 연산자를 통해서 타입 추론에 의거해 타입 좁히기 방식이 사용되어 매개변수가 특정 속성을 갖고 있다면 해당 타입으로 인지를 하고, 그에 맞는 명령을 실행하도록 되어있다. 하지만 위와 같이 주석을 달아놓지 않거나 타입 추론과 타입 좁히기를 통해 알게된 해당 타입과 내부 속성을 일일이 찾아보기에는 효율성이 떨어지며 코드의 직관성이 떨어진다. 이는 코드 자체의 오류는 발생하지 않지만 협업 시에 굉장한 불편함을 초래한다.

따라서 서로소 유니온 타입을 사용하여 코드를 다음과 같이 개선할 수 있다.

type Admin ={
  tag:"ADMIN"
  name: string,
  kickCount: number
}
type Member = {
  tag:"MEMBER"
  name: string,
  point: number
}
type Guest = {
  tag:"GUEST"
  name: string,
  visitCount: number;
}
type User = Admin | Member | Guest;

function login1(user:User){
  switch(user.tag){
    case "ADMIN":{
      console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
      break;
    }
    case "MEMBER":{
      console.log(`${user.name}님 현재까지 ${user.point}모았습니다.`);
      break;
    }
    case "GUEST":{
      console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
      break;
    }
  }
}
Type Alias로 정의한 각 타입들 내부에 tag라는 속성은 String Literal Type으로 정의되었기에 다른 타입들 간의 교집합이 존재할 수 없다. 따라서 더 직관적으로 알아보기 쉽고 타입별로 확실하게 구분을 지을 수 있기에 로그인 유효성 검사 같은 기능에서 유용하게 사용할 수 있다.

 

동시에 여러가지 상태를 표현하는 객체의 타입을 정의할 때에는 Optional Property를 사용하는 것 보다 각 상태에 따른 타입을 각각 정의를 하여 서로소 유니온 타입으로 정의하여 사용하는 것이 더 좋다. 이것 또한 코드를 보며 직관적인 이해를 해보도록 하자.

type AsyncTask = {
  state: "LOADING" | "FAILED" | "SUCCESS",
  error?: {
    message: string
  },
  response?: {
    data: string
  }
}
function processResult(task: AsyncTask){
  switch(task.state){
    case "LOADING": {
      console.log("Now Loading..")
      break;
    }
    case "FAILED":{
      console.log(`error: ${task.error?.message}`); // Error
      break;
    }
    case "SUCCESS":{
      console.log(`success: ${task.response!.data}`); // Error
      break;
    }
  }
}
위의 코드는 데이터 통신 결과(상태)에 따른 조건 처리를 나타낸 코드이다. state 값이 FAILED일 경우 error 객체에 접근하여 message를 출력하고, SUCCESS인 경우 response 객체에 접근하여 data 값을 출력해주어야 한다. 하지만 AsyncTask 타입에 error 속성과 response 속성은 Optional Property로 정의되어 undefined 값일 경우가 있기에 Optional Chaining이나 Non Null 연산 처리를 해주지 않으면 오류가 발생한다.

이런 번거로운 경우를 피하기 위해 코드를 다음과 같이 개선할 수 있다.

type LoadingTask = {
  state: "LOADING"
};
type FailedTask = {
  state: "FAILED"
  error: {
    message: string
  }
};
type SuccessTask = {
  state: "SUCCESS"
  response: {
    data: string
  }
};
type AsyncTask = LoadingTask | FailedTask | SuccessTask;

function processResult(task: AsyncTask){
  switch(task.state){
    case "LOADING": {
      console.log("Now Loading..")
      break;
    }
    case "FAILED":{
      console.log(`error: ${task.error.message}`);
      break;
    }
    case "SUCCESS":{
      console.log(`success: ${task.response.data}`);
      break;
    }
  }
}
각 state에 따른 타입을 별도로 정의를 하여 서로소 유니온 타입으로 분리를 하면 각 상태 값에 따라서 속성 값들에 대해 쉽고 더 직관적으로 접근이 가능하다.

 

이렇게 비슷한 기능을 하며 같은 속성을 갖고 있는 서로 다른 타입들을 통해 기능을 구현할 때, 각 타입별로 겹치거나 객체의 속성값들에 대한 추가 처리를 해 줄 필요가 없게 만들어주는 타입 및 방식이라고 생각하면 이해가 쉬울 것 같다.

'Typescript' 카테고리의 다른 글

[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31
[TypeScript] - (2) Typescript 기본  (3) 2024.07.23
[Typescript] - (1) Typescript 사용하기  (2) 2024.07.19
본 포스팅은 Inflearn(인프런) 이정환님의 TypeScript 강의를 기반으로 작성되었습니다. 

[원시 Type과 리터럴 Type]

String Type

const a:string = "Hello";

Number Type

const a:number = 10;

Boolean Type

const a:boolean = false;

Null Type

const a:null = null;

Undefined Type

const a:undefined = undefined;

Literal Type

- "literal == 값"이기 때문에 특정 값으로만 지정된 타입을 의미한다.

- 이 타입은 많이 사용되지는 않는다.

let num2: 10 = 10;
let str2: "hi" = "hi";
let bool: true = true;

tsconfig.json 파일의 "compilerOptions"에 "strictNullChecks" 속성에 true를 입력하면 null type에 대한 검사를 엄격하게 진행한다는 설정이고 false를 입력하면 검사를 하지 않겠다는 설정이 된다.

"compilerOptions" 속성에는 "strict"라는 속성이 있는데 "strict"라는 단어가 들어간 모든 속성들의 최상위 속성이므로 strictNullChecks":false 같이 별도로 값을 지정해주지 않으면 모든 strict 관련 속성들은 "strict"에 할당된 값으로 적용이 된다.

[배열과 튜플 Type]

Array Type

- 일반 타입 선언과 제네릭 타입 선언이 있지만 일반 타입 선언이 코드의 길이를 단축시키기에 좋다.

const arr:number[] = [1,2,3];
//제네릭 타입
const arr:Array<number> = [1,2,3];

배열에 들어가는 요소의 타입이 한가지가 아닌 경우

- Union Type(유니온 타입) 사용

//Union Type
const arr:(number | string)[] = [10,'hello'];

다차원 배열

- 내부 배열의 타입을 정해준 후 배열의 깊이에 맞게 대괄호를 연속적으로 붙여준다.

const arr:number[][] = [
  [1,2,3],
  [4,5]
];

 

Tuple(튜플)

- 길이와 타입이 고정된 배열

const arr:[number,string] = [10,'hello'];
const arr1:[number,string,boolean] = [10,'a',true];
const arr1:[string,boolean,number] = [10,'a',true]; //Error

튜플은 TS에만 존재하는 자료형으로 튜플을 사용한 TS 파일을 컴파일하여 JS 파일로 변환 시 일반 배열 형태로 컴파일된다. 튜플은 사실상 일반 배열과 같으며 길이가 고정되어 있음에도 JS에서 사용하는 배열 method도 동일하게 사용이 가능하다. 따라서 튜플을 사용할 때에는 각별한 주의가 필요하다.

튜플은 배열을 사용할 때에 배열 안의 요소들의 위치가 정해져 있고 요소들의 순서가 중요한 경우에 사용하기에 용이하다.

//Tuple Type 예시
const user:[string,number][] = [
  ['tom',1],
  ['mike',1],
  ['jun',1],
  ['kelly',1],
  [5,'bob'], //Error
]

[객체 Type]

Object

객체 타입은 타입명을 object로 정의하여 사용할 수 있다. 하지만 객체는 기본적으로 내부 property들을 보유하고 있다. object로 타입을 지정해두고 해당 객체의 속성과 속성값에 접근하려고 하면 오류가 발생한다. 예시는 다음과 같다.

const user:object = {
  id: 1,
  name: 'Tom'
}
user.id; //Error

object로 타입을 지정하면 변수 자체를 객체로는 취급하지만 내부 속성에 대해서는 아무 정의도 하지 않기 때문이다.

따라서 객체 타입을 정의하기 위해서는 객체 리터럴 타입으로 정의해주어야 한다.

//Object Literal Type
const user: {
  id: number,
  name: string
} = {
  id: 1,
  name: 'Tom'
}

위와 같이 객체 리터럴 타입으로 정의를 해주어야하는 이유는 TS는 객체를 정의할 때 객체와 그 내부의 속성을 따지는 즉, 객체의 구조를 따져서 타입을 정의하기 때문에 구조적 타입 시스템을 따르기 때문이다.

(*C언어와 Java 같은 언어는 이름에 따른 타입을 정의하기 때문에 명목적 타입 시스템을 따른다고 한다.)

Optional Property(선택적 속성)

객체를 생성하다보면 내부 속성중에 필수 속성도 있지만 있어도 되고 없어도 되는 선택적인 속성도 존재하기 마련이다. 그런 속성을 다루기 위한 방식이 선택적 속성(Optional Property)이다. 선택적 속성은 ES6 문법에 존재하는 Optional Chaining을 사용한다. 사용 방법은 아래와 같다.

const user:{
  name:string;
  age:number;
} = {
  name: 'Tom'
} //Error

//Optional Property(with Optional Chaining)
const user:{
  name:string;
  age?:number;
} = {
  name: 'Tom'
}

Readonly

객체를 다루다보면 객체 내부 속성의 속성값을 변경하는 경우도 발생한다. TS에서도 당연히 정의된 타입과 변경하고자 하는 속성값의 타입이 일치한다면 문제 없이 가능하지만 개발을 하는 과정에서 절대적으로 변경되어서는 안되는 객체의 속성값들을 안전하게 수정 불가 상태로 보호하는 방식이 존재하는데, 그것이 바로 Readonly 키워드이다.

Readonly는 말 그대로 '읽기 전용' 속성이므로 속성값을 변경할 수 없도록 막아준다. 사용 방법은 아래와 같다.

const config:{
  api: string;
} = {
  api: "a123b";
}
config.api = "hello";
console.log(config.api) //"hello"

//Readonly
const config:{
  readonly api: string;
} = {
  api: "a123b";
}
config.api = "hello"; //Error
console.log(config.api) //"a123b"

 

Readonly 속성은 배열,튜플,객체의 속성에만 적용이 가능하며 원시 타입의 자료형에는 사용이 불가능하다.

[타입 별칭(Type Alias)과 인덱스 시그니처]

Type Alias(타입 별칭)

Type Alias(타입 별칭)불필요한 타입 정의의 반복을 피하기 위해 사용되는 방식이다. 다양한 변수들을 선언하고 타입을 정의해주는 경우에 같은 테마의 변수들은 내부 속성들의 구조와 타입들이 같을 것이다. 그러한 경우에 타입을 반복적으로 사용할 필요 없이 Type Alias를 사용하면 된다. 사용 예시는 다음과 같다.

let user:{
  id:number,
  name: string,
  nickname: string,
  birth: number,
  bio: string,
  location: string
} = {
  id: 1,
  name:'mike',
  nickname: 'he',
  birth: 970407,
  bio: 'home',
  location: 'korea'
}
let user1:{
  id:number,
  name: string,
  nickname: string,
  birth: number,
  bio: string,
  location: string
} = {
  id: 2,
  name:'tom',
  nickname: 'she',
  birth: 987842,
  bio: 'home',
  location: 'korea'
}
//Type Alias
type User = {
  id:number,
  name: string,
  nickname: string,
  birth: number,
  bio: string,
  location: string
}

let user:User = {
  id: 1,
  name:'mike',
  nickname: 'he',
  birth: 970407,
  bio: 'home',
  location: 'korea'
}
let user1:User = {
  id: 2,
  name:'tom',
  nickname: 'she',
  birth: 987842,
  bio: 'home',
  location: 'korea'
}

Index Signature(인덱스 시그니처)

규칙을 갖고 움직이는 객체의 타입을 정의할 때 사용하는 방식이다. 객체 내부의 속성과 속성값의 타입이 굉장히 많은 경우에 객체에 포함된 모든 속성과 속성값의 타입이 고정적이지만 동일한 타입을 무수히 많이 정의하는 것은 효율적이지 못하다. 그런 경우에 Index Signature 방식을 사용하여 객체 속성의 타입을 정의해주면 된다. 사용 예시는 아래와 같다.

type CountryCodes = {
  [key:string] : string
}
let countryCodes:CountryCodes = {
  korea: 'ko',
  usa: 'us',
  japan: 'jp'
}
let countryCodes1:CountryCodes = {}; // Not Error

위의 예시에서 속성명인 국가 이름과 속성값인 국가 코드는 모두 string 형태의 변수이며 추가되는 속성들도 모두 같을 것이기 때문에 국가 별로 일일이 작성하는 것이 아닌 Index Signature 방식을 이용해서 효율적으로 타입을 정의하는 것이다.

 

Index Signature는 타입의 형태만 정의하는 것이기 때문에 객체 내부가 비어있어도 오류를 발생시키지 않는다.

 

수많은 속성중에서도 필수로 포함되어야 하는 속성이 있을 수 있다. 그런 속성은 아래와 같이 Type Alias에 별도로 분리하여 정의해주면 된다. 이러한 경우에는 객체 내부가 비어있으면 오류를 발생시킨다.

type CountryNumbers = {
  [key:string]: number,
  korea: number // 필수로 포함되어야하는 property를 작성하는 방법
}
let countryNumbers:CountryNumbers = {
  korea: '82',
  usa: '00',
  japan: '01'
}
let countryNumbers1:CountryNumbers = {}; //Error
let countryNumbers:CountryNumbers = {
  usa: '00',
  japan: '01'
} // Error

 

하지만 필수로 포함되어야 하는 속성의 Type이 Index Signature로 정의된 Type과 일치하지 않는 경우는 오류가 발생한다. 따라서 Type은 일치거나 호환되도록 정의해주도록 주의해야한다.

(객체 타입의 호환에 대해서는 다음 글에서 다룰 예정이다.)

type CountryNumbers = {
  [key:string]: number,
  korea: string // Error
}

[Enum Type]

Enum Type

- 여러가지 값들에 각각 이름을 부여해 열거해두고 사용하는 타입

 

<숫자형 Enum>

enum 타입에 특정 명칭으로 정의를 해두면 enum 타입 내부의 값들은 자동으로 0부터 번호가 매겨지게 된다.

특정 값에 임의의 번호를 부여해주면 그 다음 값들은 순차적으로 값이 매겨지게 된다. 아래에 예시를 보자.

enum Role {
  A,
  B,
  C = 10,
  D
}
const user1 = {
  name: "A",
  role: Role.A
}
const user2 = {
  name: "B",
  role: Role.B
}
const user3 = {
  name: "C",
  role: Role.C
}
const user4 = {
  name: "D",
  role: Role.D
}
console.log(user1); //{name: "A", role: 0}
console.log(user2); //{name: "B", role: 1}
console.log(user3); //{name: "C", role: 10}
console.log(user4); //{name: "D", role: 11}

<특정 값 Enum>

enum 타입에 숫자 값이 아닌 다른 값을 부여하는 것도 가능하다.

enum Language {
  Korea: "ko",
  English: "en"
  Japan: "jp"
}
const one = { lan: Language.Korea };
const two = { lan: Language.English };
const three = { lan: Language.Japan };
console.log(one.lan); // "ko"
console.log(two.lan); // "en"
console.log(three.lan); // "jp"

 

앞서 보았던 내용들에 따르면 Type Alias와 같이 별도로 정의해둔 타입들은 TS 파일을 JS 파일로 컴파일하는 과정에서 사라진다고 알고 있기에 Enum Type의 값을 어떻게 사용해도 오류가 나지 않는지에 대한 의문을 가질 수 있다. Type Alias를 비롯하여 변수에 정의한 타입들은 컴파일 과정에서 사라져 JS 파일에 나타나지 않지만, Enum Type은 컴파일 과정을 거쳐도 JS 파일에서 사라지지 않고 객체 형태로 변환이 된다. 따라서 객체의 속성값을 사용하듯이 Enum Type에 정의된 값을 사용할 수 있는 것이다.

[Any와 Unknown Type]

Any Type

- 특정 변수의 타입을 확실이 모를 때 사용하는 타입이며 모든 타입과 호환이 가능하지만 굉장히 위험한 타입이다.

Any Type은 모든 타입의 슈퍼 타입이자 서브 타입이 가능하기에 치트키와 같아서 타입 검사를 안하는 것과 다를 바가 없어서 런타임 오류가 발생할 확률이 높은 타입이다.

let anyVar: any = 10;
let num:number = 10;

anyVar = "hello"
anyVar = true;
anyVar = () => {}
num = anyVar; //Not Error
anyVar.toUpperCase(); //Not a Syntax Error, Runtime Error

위의 예시를 보면 anyVar이라는 변수에 모든 타입의 변수가 할당이 가능하여 어떤 값을 넣거나 어떤 method를 사용해도 문법적 오류는 발생하지 않는다. 하지만 해당 파일을 실행하였을 경우 런타임 에러가 발생한다.

따라서 사용하는 것을 지양하는 것이 좋다. 

Unknown Type

- Any Type과 같이 모든 타입으로 사용이 가능하여 모든 타입의 값을 할당 받을 수 있지만 모든 타입에 할당을 할 수는 없다.

(타입 좁히기라는 방식을 통하여 다른 타입의 변수에 할당하여 사용할 수는 있지만 타입 좁히기에 관해서는 다음에 다룰 예정이다.)

[Void와 Never Type]

Void Type

- 공허, 빈 것을 의미하는 단어로 아무것도 없음을 의미하는 타입이다.

Void Type으로 정의한 변수에는 undefined엄격한 null 값 검사 옵션이 false인 경우에는 null까지만 할당이 가능하다.

아무것도 없는 타입이므로 변수를 할당하는 것이 불가능한 것이다. 이 뿐만 아니라 함수의 return 값의 타입을 정의하는데, 함수의 return 값이 없는 경우에 Void Type을 사용한다.

우리는 JS에서 아무것도 없는 값에 대해서는 undefined와 null을 사용한다고 알고 있는데 Void Type이 필요한 이유는 무엇일까?

TS에서 함수의 return 값에 대한 타입을 undefined나 null 타입으로 정의를 해버리면 그 함수는 return 값이 없어도 되는 것이 아닌 undefined나 null 값 자체만을 반환해야한다. 반환 값이 없거나 모를 때 사용이 불가능하기 때문에 Void Type을 사용하는 것이다.

 

Never Type

- 존재하지 않는, 불가능한 타입을 의미한다.

Never Type은 타입 계층도에서 보았듯이 모든 타입의 서브 타입이기 때문에 그 어떤 타입 및 값도 할당받을 수 없다. 심지어 null 타입과 any 타입까지 모두 불가능하다. Never Type은 자체로 독립적인 타입이다.

이런 Never Type은 우리가 코드를 작성할 때 에러를 반환하는 함수나, 종료 자체가 불가능한 예를 들어 값의 반환이 없는 특정 반복과 같은 단순 기능만을 하는 함수의 return 값의 타입으로 사용된다.

function Loop():never {
  while(true){
   ...
  }
}
function Error():never {
  throw new Error();
}

'Typescript' 카테고리의 다른 글

[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31
[Typescript] - (3) Typescript 이해하기  (0) 2024.07.26
[Typescript] - (1) Typescript 사용하기  (2) 2024.07.19
본 포스팅은 Inflearn(인프런) 이정환님의 TypeScript 강의를 기반으로 작성되었습니다.

[타입스크립트?]

  • 타입스크립트는 2012년에 MS 개발자이자 C# 언어의 창시자인 덴마크인 앤더스 하일스버그(Anders Hejlsberg)가 만든 언어로 JS에 타입관련 기능들을 추가하여 JS의 확장판으로 안정성을 갖고 더 안전하게 사용할 수 있도록 언어이다.
  • 자바스크립트는 브라우저 내에서 간단한 기능만을 위해서 발명된 언어이며 nodeJS라는 JS 런타임 환경이 생기면서 어디서든 JS를 구동 가능하고 다양한 웹앱을 만들 수 있게 되었지만 유연한 문법에 자유롭고 버그 발생 가능성이 높았기에 안정성이 부족하고 안전하지 못한 언어라는 단점이 있다.
  • 그래서 나오게 된 언어가 TS이며 간단히 말해 JS에서 변수의 타입만 정해주면 TS가 된다.

[타입 시스템에 따른 JS의 한계와 TS]

타입 시스템

- 정적 타입 시스템: 코드 실행 이전 모든 변수의 타입을 고정적으로 결정하는 엄격하고 고정적인 시스템.(ex/ C, C#, Java)

1. 코드 실행 이전에 모든 변수의 타입을 결정함으로 코드를 작성할 때 모든 변수의 타입을 결정해주어야 함.
2. 타이핑 양이 매우 증가함.

 

- 동적 타입 시스템 : 코드를 실행하고 나서 그때 마다 유동적으로 변수의 타입을 결정하는 자유롭고 유연한 시스템.(ex/ Python,Javascript)

1. 변수의 타입들을 코드가 실행되는 도중에 결정함으로 우리가 직접 정의하지 않음
2. 변수의 타입이 하나로 고정되어있지 않으므로 아무 타입의 값이나 자유롭게 담을 수 있음.
3. 타입과 맞지 않는 메서드나 함수를 사용하면 해당 코드가 실행되지 않고 오류가 발생함.

 

타입스크립트는 정적 타입 시스템과 동적 타입 시스템을 합쳐서 만들어진 점진적 타입 시스템 언어이다.

점진적 타입 시스템은 모든 변수의 타입을 일일이 지정할 필요가 없는 시스템이다.

실행 전 검사를 통해 타입의 안정성을 확보하고 자동으로 변수의 타입을 추론한다.

출처: Inflearn 한 입 크기로 잘라먹는 타입스크립트


[타입스크립트의 컴파일 과정]

컴파일이란?

프로그래밍 언어로 작성된 코드를 컴퓨터 시스템이 이해할 수 있도록 변환하는 과정을 컴파일링이라고 한다.

 

컴파일러의 컴파일 과정

1. 작성된 코드를 AST로 변환한다.

2. AST 코드를 바이트 코드 및 아스키 코드 등 컴퓨터가 읽을 수 있는 코드로 변환한다.

(*AST(추상 문법 트리) - 코드에 의미 없는 부분을 제외하고 저장하는 과정을 거치는 트리구조의 자료형태)

 

타입스크립트의 컴파일 과정

1. 작성된 코드를 AST로 변환된 이후 타입 검사를 진행한 후 JS로 변환한다.

2. 타입 검사 성공시 JS는 AST로 변환되고 바이트 코드로 변환된 후 실행이 되고 타입 검사 실패시 컴파일이 종료된다.

타입과 관련된 코드들은 컴파일 결과시에 모두 사라지므로 타입 검사에 오류가 발생되지 않는다.

[tsconfing.json, 컴파일러 옵션 설정하기]

명령어

npm init(npm init -y)

- nodeJS 패키지 초기화(-y는 모든 설정을 기본 설정으로 하겠다는 의미)

npm i @types/node

- 타입 정보를 갖고 있는 npm 라이브러리 설치

npm i typescript -g

- 타입스크립트 컴파일러(tsc(=typescript complier)) 설치

tsc {폴더}/{파일명}

- ts 파일 컴파일링

node {폴더}/{파일명}

- js 파일 실행

npm install ts-node -g

- tsc와 nodeJS가 함께 있는 컴파일링과 실행이 동시에 가능한 패키지 설치

ts-node {폴더}/{파일명}

- ts 파일 즉시 실행

tsc --init

- tsconfig.json 파일 자동 생성 명령어

 

tsconfig.json 초기 설정 및 속성 설명

{
  "compilerOptions": {
    "target": "ESNext", //최신 버전의 Javascript로 설정, ES버전에 따라 설정가능
    "module": "ESNext", //컴파일하는 파일의 모듈 옵션 설정,최신 버전의 Javascript로 설정,CommonJS나 ES사용 가능
    "strict": true, //엄격한 타입 검사 옵션, JS를 TS로 마이그레이션 하는 경우는 false로 설정해두기도 한다.
    "outDir": "dist", // 컴파일 한 파일들의 결과(.js)를 모아둘 파일
    "moduleDetection": "force"
    //타입스크립트는 기본적으로 모든 파일을 전역 모듈로 보기 때문에 각 파일을 독립적인 모듈로 인식하는 옵션
    //이 옵션이 아니라면 모든 ts 파일에 export {}; 를 마지막에 넣어주면 된다.
  },
  "include": ["src"] //컴파일 할 파일들이 있는 경로 설정
}

 


실행 오류

ts-node를 실행했을 때 다음과 같은 오류가 발생한다.

그 이유는 nodeJS에서 ESM 모듈 시스템을 사용하기 위해서는 package.json 파일에 "type": "module"을 입력해주어야 JS에서 ESM을 사용할 수 있었듯이 TS에서도 마찬가지로 입력해주어야 모듈을 설정해주어야 하기 때문이다.

다시 실행을 했을 때 또 오류가 발생한다.

ts-node를 사용할 때에는 tsconfig.json 파일에 "ts-node" : {"esm": true} 속성을 추가로 입력해주어야 한다.타입스크립트는 기본적으로 CJS를 사용하기 때문에 ESM을 이해하지 못하기 때문이다.

또한 tsc 명령어만 입력하여 ts 파일을 컴파일링한 후 원하는 dir에 컴파일한  파일을 넣는 작업을 처리했을 때 다음과 같은 오류가 발생하기도 한다.

@types 버전이 20버전 이상으로 업데이트되면서 특정 라이브러리에서 타입 검사 오류가 발생하는 것으로 compilerOption 내부에 "skibLibCheck": true 옵션을 추가 해 주어야 한다. 이 옵션은 타입 정의 파일(.d.ts 확장자를 갖는 파일을 의미)의 타입 검사를 생략하는 옵션이다. 보통 타입 정의 파일은 라이브러리에서 사용하는데 가끔 라이브러리의 타입 정의 파일에서 타입 오류가 발생하는 일이 발생할 수 있다. 따라서 해당 옵션을 true로 설정하여 불필요한 타입 정의 파일의 타입 검사를 생략하도록 설정해야 한다.
새로운 프로젝트를 진행하게 되어 tsconfig.json 파일을 생성하게 된다면 역시나 추가로 입력해주어야 하는 설정이다.

23년 10월 Node.js의 LTS(장기 지원 버전)가 20대로 업데이트 되며 ts-node가 정상적으로 동작하지 않기 때문에 ts-node tsx를 사용해야한다. 사용 방법은 동일하니 설치만 하면 되고  설치 이후에는 명령어에서 ts-node를 tsx로 대체해 주면 된다.
npm i -g tsx

 

- tsx 설치 명령어

tsx {폴더}/{파일명}

- ts-node를 대체하여 tsx 사용 명령어

'Typescript' 카테고리의 다른 글

[TypeScript] - (6) Class  (0) 2024.08.05
[Typescript] - (5) Interface  (0) 2024.08.01
[Typescript] - (4) Typescript 함수  (0) 2024.07.31
[Typescript] - (3) Typescript 이해하기  (0) 2024.07.26
[TypeScript] - (2) Typescript 기본  (3) 2024.07.23

+ Recent posts