포스트

Typescript 5 튜토리얼 - 1일차

using, satisfies 같은 새로운 키워드도 나오고 버전도 계속 올라가면서 보수 교육이 필요해졌다. 최신 문서들을 참고해 공부하고 기록해보자.

Typescript 5 튜토리얼 - 1일차

목록

1. Typescript 기능들

최근 5.2 beta 버전까지 나왔다. 주요 기능 위주로 빠르게 살펴보자

1) 데코레이터

클래스 및 클래스 멤버에 대해서 데코레이터를 정의하고 적용할 수 있다.

  • 다중 적용시 위에서 아래로, 포함하는 관계로 실행순서가 정해진다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}
 
function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}
 
class ExampleClass {
  @first()
  @second()
  method() {}
}

// -----------------------------
// # 출력 결과
// -----------------------------
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called

class 데코레이터 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ClassDecorator
class A {
  b: string = "Hello"

  get c(): string {
    return `${this.b} World!`
  }

  d(e: string): void {
    console.log(e)
  }
}

function ClassDecorator(constructor: typeof A) {
  console.log(constructor)
  console.log(constructor.prototype)
}

// -----------------------------
// # 출력 결과
// -----------------------------
// [Function: A]
// { c: [Getter], d: [Function (anonymous)] }

2) as constkeyof 로 유니온(Union) 타입 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 객체로 as const 선언
const status = {
  todo: 'todo',
  inProgress: 'inProgress',
  complete: 'complete',
  cancel: 'cancel',
} as const;

// 또는 배열로 as const 선언
const status = ['todo', 'inProgress', 'complete', 'cancel'] as const;

// 유니온 타입 생성
type Status = typeof status[keyof typeof status];
// 'todo' | 'inProgress' | 'complete' | 'cancel'

as const 으로 literal type 추론

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// const title:"typescript" 리터럴 타입으로 추론됨
const title = 'typescript';

// as const 를 이용하면 let 을 사용해도 리터럴 타입으로 추론됨
let title = 'typescript' as const;

// as const 를 이용하여 export 하면
const Colors = {   
    red: "RED",   
    blue: "BLUE",   
    green: "GREEN", 
} as const;

// key 가 자동으로 추출되어 자동으로 멤버 추론을 할 수 있다
Colors.blue  // 또는 red, green

유니온 타입과 enum 타입 (가독성)

literal 타입 대신 기억하기 쉬운 korean 같은 이름을 사용하려면 enum 을 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 유니온 타입 예제
type LanguageCode = 'ko' | 'en' | 'ja' | 'zh' | 'es'
const code: LanguageCode = 'ko'

// 이렇게 하면 언어 코드가 위아래에 중복되고 코드가 너무 길어진다
const korean = 'ko'
const english = 'en'
const japanese = 'ja'
const chinese = 'zh'
const spanish = 'es'
type LanguageCode = 'ko' | 'en' | 'ja' | 'zh' | 'es'
const code: LanguageCode = korean

// enum 을 사용하면 코드 가독성을 높일 수 있다
export enum LanguageCode {
  korean = 'ko',   
  english = 'en',
  japanese = 'ja',
  chinese = 'zh',
  spanish = 'es',
}
// enum 도 as const 처럼 리터럴 타입을 갖게 한다
// (의미상) LanguageCode === 'ko' | 'en' | 'ja' | 'zh' | 'es'
const code: LanguageCode = LanguageCode.korean

const keys = Object.keys(LanguageCode) // ['korean', 'english', ...]
const values = Object.values(LanguageCode) // ['ko', 'en', ...]

단, enum 을 사용하면 객체가 생성됩니다. 그래서 union type 을 추천

union type 을 채택하면 가질 수 있는 장점

  • 객체가 생성되지 않고,
  • import 를 할 필요가 없다

2. Typescript 4.9 새로운 기능

  • satisfies 연산자
  • in 연산자를 사용한 목록에 없는 프로퍼티 좁히기
  • 클래스의 자동 접근자 accessor
  • NaN에 대한 동등성 검사
  • 파일 감시에 파일 시스템 이벤트를 사용
    • 폴림 방식 fs.watchFile, 이벤트 방식 fs.watch

1) satisfies 연산자

  • keys 와 values 에 대한 타입 검사를 한번에 처리
    • 예) bleu 오타 검사와 각 value 의 타입 검사까지 한방에 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 기존 방식 : key 오타만 잡아낼 수 있었다
const palette: Record<'red' | 'green' | 'blue', [number, number, number] | string> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255], // 오타 발견!
};

// 배열 [number, number, number] 일수도 있어서 타입 오류
const greenNormalized = palette.green.toUpperCase();

// satisfies 타입 연산자 사용하여 keys, values 를 동시에 만족하는지 검사
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255], // blue 오타
} satisfies Record<'red' | 'green' | 'blue', [number, number, number] | string>;

SvelteKit 의 TS 코드에서 사용되는 satisfies 예제

  • parameters 의 Record 구조체와 return 타입까지 한번에 타입 검사 가능
1
2
3
4
5
6
7
8
9
10
import type { PageLoad } from './$types';

export const load = (({ params }) => {
    return {
        post: {
            title: `Title for ${params.slug} goes here`,
            content: `Content for ${params.slug} goes here`
        }
    };
}) satisfies PageLoad;

in 연산자로 유니온 타입 결정하기

RGB 와 HSV 두가지 타입이 가능한 경우 key 존재 유무로 HSV 타입 결정 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface RGB {
  red: number;
  green: number;
  blue: number;
}

interface HSV {
  hue: number;
  saturation: number;
  value: number;
}

function setColor(color: RGB | HSV) {
  if ("hue" in color) {
    // 'color'의 타입은 HSV입니다.
  }
}

mapped type 에서 in 연산자 사용 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
type Fruit = 'apple' | 'banana' | 'orange';

// mapped type 선언 : in 연산자로 Fruit 타입 추론
const PRICE_MAP: { [fruit in Fruit]: number } = {
  apple: 1000,
  banana: 1500,
  orange: 2000,
};

// mapped type 활용
function getDiscountedPrice(fruit: Fruit, discount: number) {
  return PRICE_MAP[fruit] - discount;
}

NaN 대신에 Number.isNaN 을 사용

기존에 모호했던 NaN 비교 대신에 Number.isNaN 을 사용

1
2
3
4
5
6
7
8
9
10
function validate(someValue: number) {
  return someValue !== NaN;
  //     ~~~~~~~~~~~~~~~~~
  // 에러: 이 조건은 항상 'true'를 반환할 것입니다. (❌)
}

// 당신이 원하는 것은 '!Number.isNaN(someValue)' 아닌가요?
function validate(someValue: number) {
  return !Number.isNaN(someValue);
}

2) 타입스크립트 5.1

return 생략 가능 ⇒ 반환 타입 undefined

1
2
3
4
5
function foo() {
    // no return
}
// x = undefined
let x = foo();

자동 자원 해제를 위한 using 키워드

참고: Typescript 의 새로운 키워드 ‘using’

  • let, const 변수 선언 키워드에 함께 사용
  • 자원 해제 후에도 handler 에 접근할 수 있는 문제를 방지
  • 블록 스코프를 벗어날 때 자동으로 자원을 해제해 주는 새로운 기능
    • 반복적인 패턴을 줄여주고, 자원 해제를 잊는 경우를 방지하기 위한 목적
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 반복적인 패턴 : 자원 할당과 해제
try {
  const resource = getResource();
  try {
    const reader = resource.getReader();
    // ...
  } finally {
    reader.close();
  }
} finally {
  resource.release();
}

// -------------------------------------

// using 키워드로 간편하게 작성
{
  using resource = getResource();
  using reader = resource.getReader();
  const { value, done } = reader.read();
} 
// 블록을 벗어날 때 'reader' => 'resource' 순으로 자원 해제
// 이후로 resource, reader 접근을 할 수 없다

3) 타입 계층 트리

Typescript 타입 계층 트리 Typescript 타입 계층 트리

unknown 타입이 최상위 계층에 있다. ⇒ any 타입에만 할당 가능

unknown 타입 필요성

사용자로부터 입력을 받거나 잘 알려지지 않은 외부 API를 사용하는 등 실제로 어떤 값이 올 지 모를 때, 어떤 값이든 할당할 수 있지만 정작 이후에 그 값을 사용할 때는 타입 체킹을 해야만 안전하게 사용할 수 있는 타입이 필요해서 만들게 되었다고 한다. (any 보다 제한적인 타입)

upcast vs downcast 개념 그림

타입 할당 - up, down 타입 할당 - up, down

2. as 를 제거하는데 도움되는 방법 - 타입 가드

1
2
3
4
5
6
7
interface Book {
  id: number;
  author: string;
  publisher?: string;
}

const book = fetch("baseUrl/get-book?id=5") as Book;

1) as 는 assertion 타입이다

  • 데이터 변환을 수행하지 않는다
  • assertion 이 가능한지만 확인한다

주요 용도는 (보호나 보장이 아닌) 개체의 가상 유형에 대해 알려주는 것이다.

2) typeof, instanceof 는 타입 가드의 기본 연산자

1
2
3
4
typeof 90 === 'number'
typeof "abc" === 'string'

new Date() instanceof Date === true

typeof 타입가드를 활용한 함수 예제

1
2
3
4
5
6
7
function getAgeText(age: number | string) {
  if (typeof age === "number") {
    return age.toFixed(2);
  } else {
    return age.trim();
  }
}

Error 또는 assert 로도 타입가드를 기술할 수 있다.

1
2
3
4
5
6
7
8
9
10
// undefined 이면 Error 
function assert(value: any, errorMsg: string): asserts value {
  if (!value) throw new Error(errorMsg);
}

// undefined 이면 assertion Error 
function toString(value?: number) {
  assert(value !== undefined, "value 는 undefined 가 아니어야 한다.");
  return value.toFixed(2);
}

3) type-guard 이용한 Book 검증 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const isBook = (element: unknown): element is Book =>
    // Object.prototype.hasOwnProperty.call(element, "author") &&
    // Object.prototype.hasOwnProperty.call(element, "id") &&
    hasAttributes(element, ["author", "id"]) &&
    typeof element.author === "string" &&
    typeof element.id === "number";

const gift: unknown = getGift();

if (isBook(gift)) {
    /*
    * In this if-block the TypeScript compiler actually
    * resolved the unknown type to the type Book
    */

    read(gift);
    // read needs an argument of type Book

    return gift.author;
}

jest 사용시

1
2
3
4
5
6
7
8
9
10
it("should return true if the element is a book", () =>{
  const element1 = { author: "alpha"};
  expect( isBook(element1)).toBe(false);

  const element2 = {"id": 2000};
  expect( isBook(element2)).toBe(false);

  const element3 = { author: "alpha"; "id": 2000};
  expect( isBook(element3)).toBe(true);
})

또 다른 예제: String Array 타입 가드

1
2
3
4
5
6
7
8
9
10
11
12
const myValue: unknown = JSON.parse(`[ "a", "b", "c" ]`)

function isStringArray(array: unknown): array is string[] {
  if (!Array.isArray(array)) return false

  const hasNonStringElement = array.some(element => typeof element !== 'string')
  return !hasNonStringElement
}

if (isStringArray(myValue)) {
  console.log(myValue.join(''))
}

3. TypeScript 모범 사례 — 스위치, 표현식 및 다운캐스팅

대괄호와 문자열 속성값으로 접근해서는 안된다

1
2
3
4
5
// 이렇게 쓸 수도 있겠지만, 하지 말아야 한다 (❌)
obj['prop']  

// 올바른 사용법
obj.prop

오류 생성시 문자열 값을 사용하지 마라

1
2
3
throw 'error';  // 이렇게 쓸 수도 있겠지만, ❌

throw new Error("error");  // 올바른 사용법

Switch 의 Case 를 통과해서는 안된다

break 또는 return 문이 들어가 있어서 격리되어야 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 이렇게 쓸 수도 있겠지만, ❌
switch (foo) {
  case 1:
    doWork(foo);
  case 2:
    doOtherWork(foo);
}

// 올바른 사용법
switch (foo) {
  case 1:
    doWork(foo);
    break;
  case 2:
    doOtherWork(foo);
    break;
}

this 를 다른 변수로 전달하지 말 것

1
2
3
4
5
6
7
8
const self = this;  // 이렇게 쓸 수도 있겠지만, ❌
setTimeout(function() {
  self.work();
});

setTimeout(() => {
  this.work();  // 올바른 사용법
});

보호되지 않는 메소드를 사용하지 말 것

이후 class 가 변경될 수 있기 때문에, 보안상 instance 생성 후 호출을 권장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo {
  public log(): void {
    console.log(this);
  }
}
Foo.log();  // 이렇게 쓸 수도 있겠지만, ❌

class Foo {
  public log(): void {
    console.log(this);
  }
}
const foo = new Foo();
foo.log();  // 올바른 사용법

// 참고: bind 를 이용한 함수 레퍼런스로 호출하는 방법
const manualLog = foo.log.bind(foo);
manualLog.log();

any 로 타입 캐스팅을 해서는 안된다 (Downcast)

Downcasting to any 의 반대는 Upcasting from any

1
2
3
4
5
6
7
8
9
10
// 이렇게 쓸 수도 있겠지만, ❌
const foo = bar as any;

// 올바른 사용법
interface Bar {
  [key: string]: string | number; 
  [index: number]: string; 
  baz: number;
}
const foo: Bar = bar;

finally 블럭에 흐름 제어문을 사용해서는 안된다

return, continue, break, throws 등은 try/catch 문에서 사용할 것

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
  doWork();
}
catch(ex) {
  console.log(ex);
}
finally {
  return false;  // 이렇게 쓸 수도 있겠지만, ❌
}

try {
  doWork();
  reutrn true;
}
catch(ex) {
  console.log(ex);
  return false;  // 올바른 사용법
}

9. Review

  • 슈퍼셋 언어라서 새로운 개념이나 필요성이 제기될 때마다 바로바로 반영된다.
  • Symbol.dispose 는 뭔지 잘 모르겠다. 쓸 일이 생기면 그 때 다시 살펴보자.

Zod 패키지

  • API 사용시 Zod 패키지로 json 검증을 하자 (런타임시 타입 안전 보장)
    • 설치: npm i zod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { z } from "zod";

// 스키마 정의
const User = z.object({
  email: z.string(),
  age: z.number(),
  active: z.boolean(),
});

User.parse({
  email: "user@test.com",
  age: 35,
  active: true,
}); // ✅ 유효성 검증 통과

User.parse({
  email: "user@test.com",
  age: "35",
}); // ❌ 유효성 검증 실패


// 스카마로부터 타입을 추론 👍
type User = z.infer<typeof User>;

function processUser(user: User) {
  User.parse(user); // 유효성 검증
  // 사용자 처리 로직
}

참고문서

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.