당신이 몰랐던 타입스크립트 18가지 이야기

혀느현스·2022년 9월 28일
3

opize 개발

목록 보기
1/1
post-thumbnail

책 '타입스크립트 프로그래밍'을 읽고 개인적으로 메모해둔 것들을 기록한 포스트입니다.

📖 책 내용

아래 내용은 타입스크립트 프로그래밍 책을 읽으면서 남긴 메모와 이를 바탕으로 추가적으로 조사한 내용입니다. 제가 타입스크립트를 어느정도 사용하고 있다보니 기초적인 내용은 생략했습니다. (필요하시다면 댓글이나 디스코드 서버로 알려주세요. 복습 겸 다시 한 번 읽고 추가하겠습니다!)

책 내용 + 외부에서 가져온 내용인지라 책 자체의 내용과 흐름과는 차이가 있습니다. 타입스크립트 프로그래밍자체에 대한 정보를 얻고 싶다면 다른 포스트를 참고해주시길 바랍니다.

함수에 달린 속성(attr) 정의하기

정확히는 호출 시그니처(Call Signatures)속성(property) 함께 정의하기 입니다.

가끔씩 함수에 추가 속성을 붙여야 하는 경우가 있습니다. 이런 경우 다음과 같이 타이핑(Typing) 할 수 있습니다.

interface funcType {
 	() => null;
    isGood: boolean
}

const func: funcType = () => null;
func.isGood = true

super로 부모 클래스의 메소드 접근

지금까지 super는 클래스의 constructor() 에서 부모 클래스의 constructor()을 사용하기 위함으로만 알고 있었는데, supmer.Method() 형식으로 부모 클래스의 메소드를 사용할 수 있다는 걸 알게되었습니다. 단 메소드(함수)에는 접근이 가능하지만 프로퍼티(값)에는 접근할 수 없습니다.

public(기본)protected 접근 제한자가 붙어있는 메소드는 사용할 수 있지만, private 접근 제한자가 붙어있는 메소드도 사용할 수 없습니다.

class Rectangle {
  	width: number;
	height: number;
	constructor(width: number, height: number) {
    	this.width = width;
      	this.height = height
    }

	getArea() {
    	return this.width * this.height
    }
}

class Square extends Rectangle {
    constructor(width: number, height: number) {
      super(width, height)
    }
  
  	getArea2() {
    	return super.getArea()
    }
}

const square = new Square(5, 5);
console.log(square.getArea()) // 25
console.log(square.getArea2()) // 25

추상 클래스는 자바스크립트 코드로도 만들어진다.

이해를 위한 매우 간단한 설명
타입 수준: 유효한 타입스크립트 코드
값 수준: 유효한 자바스크립트 코드

추상 클래스가 interfacetype 처럼 타입 수준만 있는 코드가 아니라 타입 수준값 수준이 모두 있는 것이라는 걸 알게되었습니다.

추상 클래스에서 추상 메소드가 아닌 실제 메소드 (abstract 가 붙지 않은 메소드) 를 정의한 경우 해당 클래스를 상속받은 자식 클래스가 해당 메소드에 접근할 수 있다고 하니, 사실상 추상 클래스의 목적은 해당 클래스가 인스턴스를 생성하지 못하게 제한 하는 역할이라 생각하면 될 것 같네요.

// TypeScript
abstract class Rectangle  {
  	width: number;
	height: number;
	constructor(width: number, height: number) {
    	this.width = width;
      	this.height = height
    }

	getArea() {
    	return this.width * this.height
    }

    abstract getArea2(): number
}

// JavaScript (컴파일됨)
"use strict";
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    getArea() {
        return this.width * this.height;
    }
}

값 수준 네임스페이스와 타입 수준 네임스페이스는 별개다.

저는 지금까지 값 수준 네임스페이스와 타입 수준 네임스페이스가 같다고 알고 있었습니다. 예전에 두 개를 같은 이름으로 했더니 경고가 나왔던 기억이 있었거든요. (지금 다시 해보니 경고가 발생하지 않았네요. 아마 따로 ESLintPritter 를 적용해서 그런 것 같습니다.)

따라서 함수, 변수 이름을 타입 이름과 같이 하는 방법도 있다고 합니다. import, export 할 때도 맥락(context)에 따라 알아서 분리되어 사용된다고 하니 걱정없이 사용할 수 있을 것 같습니다.

추가로 타입 수준과 값 수준의 이름을 똑같이 하는 패턴을 컴패니언 객체 패턴 (Companion Object Pattern) 이라고 합니다.

type func = () => null
const func: func = () => null

매핑된 타입 (mapped type)

이미 자주 쓰는 타입이긴 한데 그 이름이 매핑된 타입 이라는 건 책을 통해 알게 되었습니다.

추가적으로 알아보니 as 를 사용할 수 있다고 합니다. 조건부 타입을 이용해서 키를 제거할 수도 있다고 하네요.

interface A {
	[key: string]: string
}

type Week = '월' | '화' | '수' | '목' | '금' | '토' | '일'
interface FullWeek {
	[ key in Weekday as string ]: string
}

interface Weekday {
	[ key in Weekday as Exclude<key, "토" | "일"> ]: string
}

readonly 제거하기

가끔씩 readonly 인 속성을 수정 가능하게 만들어야 할 때가 있습니다. 그럴 경우 간단하게 -readonly 를 붙여주면 됩니다.

interface A {
	readonly a: string
}

interface A {
	-readonly a: string
}

유틸리티 함수 Record, Partical, Required, etc.

타입스크립트를 처음 배울 때 조금 어려운 부분이 유틸리티 함수가 뭐가 있는지 모른다는 점이었습니다.

아래 3개외의 유틸리티 함수는 https://www.typescriptlang.org/docs/handbook/utility-types.html 이곳을 참고해보세요.

아래 유틸리티 함수 (Record, Partial, Required) 외에도 Pick, Omit, Readonly, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType 등이 있습니다.

// Record<키, 값>
type A = Record<string, number>;

type A = {
   [x: string]: number;
}
// Partial<객체>
type B = Partial<{
	a: string;
  	b: string
}>

type B = {
    a?: string | undefined;
    b?: string | undefined;
}
// Required<객체>
type C = Required<{
	a?: string;
  	b?: string
}>

type C = {
    a: string;
    b: string;
}

튜플 함수 만들기

자바스크립트에는 튜플 이라는 자료형이 존재하지 않지만, 타입스크립트를 통해 튜플과 비슷하게 동작시킬 수 있습니다. 그리고 이를 이용해 튜플을 만들어내는 함수도 만들 수 있습니다.

const tuple = <T extends unknown[]>(...ts: T): T => ts

let a = tuple(1, true) // [number, boolean]

조건부 타입

삼항 연산자를 타입 수준에서도 사용할 수 있습니다.

type IsString<T> = T extends string ? true : false

분배적 조건부

타입 수준의 삼항 연산자는 받은 타입을 분배하는 특성을 가지고 있습니다. 아래 조건부 식은 그 아래 식과 동일합니다.

type Asdf<T> = (string | number) extends T ? A : B

type Asdf<T> = (string) extends T ? A : B | (number) extends T ? A : B

이를 응용하면 다음처럼 활용할 수 있습니다. 아래는 분배적 조건부를 활용한 코드와 그렇지 않은 코드의 차이점을 보여줍니다.

type ToArray<T> = T extends unknown ? T[] : T[]
type ToArray2<T> = T[]

type A = ToArray<number | string> // number[], string[]
type B = ToArray2<number | string> // (number, string)[]

만약 분배되는 것을 막으려면 extends 옆을 대괄호로 감싸면 됩니다.

type ToArray<T> = [T] extends [unknown] ? T[] : T[]

type A = ToArray<number | string> // (string | number)[]

인라인 제네릭 타입 선언 infer

<T>로 제네릭을 만들 필요 없이 infer 를 통해 제네릭을 만들 수 있습니다.

// 함수의 두 번째 인자의 타입을 가져오는 타임 합수
type SecondArg<F> = F extends (a: any, b: infer B) => any ? B : never

type func = (a: string, b: number) => void
type A = SecondArg<func> // number

내장 조건부 타입 Exclude, Extract, NonNullable, ReturnType, InstaceType

유틸리티 함수들 중 조건부 타입입니다. 이 외에도 다양한 조건부 타입이 존재합니다.

// Exclude<T, U> : T에 속하지만, U에는 없는 타입 (T - U)
type A = Exclude<string | number, string> // number
// Extract<T, U> : T 중에서 U에 할당 가능한 타입 (T - ∁U)
type B = Extract<string | number, string> // string
// NonNullAble<T> : T 중에서 null과 undefined를 제외한 타입 (T - {null, undefined})
type C = NonNullAble<string | null | undefined> // string
// ReturnType<F> : 함수의 반환타입 (단, 제네릭과 오버로드된 함수는 반응 X)
// 메모에 "제네릭과 오버로드된 함수는 반응X"라고 써있긴 한데
// 이게 따로 제네릭을 지정해주면 되는 것 같습니다.
// 혹시 자세히 알고 계시다면 댓글로 알려주시면 감사하겠습니다!
type Func = () => string
type D = ReturnType<Func> // string
// InstanceType<C> : 클래스의 인스턴스 타입

타입 단언 (Type Assertion)

특정 타입이라고 확정하는 문법입니다. 지금까지 as를 이용한 타입 단언만 알고 있었는데 다른 방법도 있었네요. (물론 추천하지 않는 방법이지만...)

아래에서 <string>input 같은 형태로 타입 단언을 하는 것은 JSX/TSX와의 혼동으로 인해 사용하지 않는 것을 추천합니다. 실제로 JSX를 사용하는 환경에서 저 문법을 쓰면 타입 단언이 아니라 JSX로 인식해서 오류가 발생합니다.

const input: string | number = 'a'

const a = input as string // a의 타입: string
const b = <string>input

그리고 타입 단언은 아무 타입으로 단언할 수 있는 것이 아닌, 해당 타입의 상위 집합이나 하위 집합에 해당하는 타입만 단언할 수 있습니다.

let a: 'text' | 'text2' = 'text'

const b = a as 'text' // OK
const c = a as string // OK

const d = a as number // Error
// 'string' 형식을 'number' 형식으로 변환한 작업은 실수일 수 있습니다. 두 형식이 서로 충분히 겹치지 않기 때문입니다. 의도적으로 변환한 경우에는 먼저 'unknown'으로 식을 변환합니다.ts(2352)

Non-Null 단언 (Non-Null Assertion)

해당 타입이 null이나 undefined가 아니라고 단언하는 데 사용합니다.

개인적으로 자바스크립트의 옵셔널 체이닝 (?.) 과 비슷하다는 생각이 들었습니다.

type A = {
	b?: string
}

const a:A = {
	b: 'a'
}

a.b // string | undefined
a!.b // string

let c: undefined | string
const d = c // undefined | string
const e = c! // string

Non-null 단언 은 함수에도 사용할 수 있습니다.

const func = (): string | undefined => 'a'

const a = func()  // string | undefined
const b = func()! // string

변수 선언에서도 사용할 수 있습니다. 주로 외부 변수를 사용하는 함수로 인해 일반적인 타입 추론이 불가능한 상황에서 사용됩니다.

// Non-null 단언 사용 X
let a: string
func()
a // undefined | string
a.split() // ERROR

// Non-null 단언 사용
let a!: string
func()
a // string
a.split() // OK![](https://velog.velcdn.com/images/phw3071/post/fecbcceb-8353-4e5a-88e3-bef4ebec6ecc/image.png)

책에서는 이 내용을 확실한 할당 단언 이라고 설명하고 있습니다. 그러나 이 이름을 검색했을 때 따로 검색되는 것이 없어 Not-null 단언에 포함했습니다.

이름 기반 타입 흉내내기

여러분도 아시다시피, 타입스크립트는 구조 기반 타입 을 사용합니다. 이 말은 즉, 구조가 같다면 같은 타입으로 여겨진다는 것입니다. 물론 이 방법이 편리한 부분도 있지만, 가끔은 다른 언어의 이름 기반 타입을 원하는 경우도 있습니다. 시스템적으로 이름 기반 타입을 사용할 수는 없지만, 심볼(Symbol)을 이용한 약간의 꼼수를 통해 비슷하게 흉내를 낼 수 있습니다.

type ID = string & { readonly brand: unique symbol }
const ID = (id: string) => id as ID

const func = (a: ID) => null
let id = ID('a')
func(id) // OK
func('a') // ERROR
// 'string' 형식의 인수는 'ID' 형식의 매개 변수에 할당될 수 없습니다.
//   'string' 형식은 '{ readonly brand: unique symbol; }' 형식에 할당할 수 없습니다.ts(2345)

내장 프로토타입 확장하기

지금까지 타입스크립트를 쓰면서 내장 프로토타입을 수정한 적은 별로 없었지만... 그래도 타입 안전하게 프로토타입을 확장할 수 있는 방법이 있다고 합니다.

다만 내장 프로토타입을 확장하는 만큼 전역으로 적용하거나, 확장된 프로토타입을 쓰려는 코드마다 아래 코드를 작성해야 합니다.

interface Array<T> {
	zip<U>(list): [T, U]()
}
Array.prototype.zip = <T, U>(...) => ... 

예외 처리 방법

타입스크립트에서 예외를 처리하는 방법에는 크게 4가지가 있다고 합니다.

  • null 반환
  • Error 던지기 (throw)
  • 예외 반환 (return)
  • Option 타입

아래는 개인적인 생각입니다.

null 반환은 return 값을 null을 반환하는 것을 의미합니다. 보통 검색을 하는 함수에서 대상을 못 찾았을 때 주로 사용합니다. 에러 정보를 포함하지 않기 때문에 디버깅이 힘들 수 있지만, 에러를 이르키지 않는 것이 장점(?)입니다.

예외 던지기는 null 반환과 같이 가장 익숙한 예외 처리 방법일 것입니다. 예외 처리를 하는 곳에서는 throw new Error() 형식으로 던지고, 받는 쪽에서는 try ... catch ... 로 처리하게 됩니다.

예외 처리를 담당하는 문법이니만큼 대부분의 코드에서 사용되지만, 조금 치명적인 문제점이 있습니다. 바로 throw로 던지는 에러의 타입을 정의할 수 없다는 점입니다. JSDoc의 @Throws 으로 어떤 에러가 발생할 수 있는지 힌트를 줄 수 있지만, 타입 평가를 하지 않으므로 '힌트'에 불과합니다. 결국 어떤 에러가 발생하는지는 개발자가 문서나 다른 방법을 통해 알아봐야 한다는 문제가 있습니다.

이러한 문제로 인해 개인적으로 내부 구현에서는 예외 반환을 사용합니다. throw를 통해 예외를 던지는 대신 return을 통해 예외를 반환합니다. 이 경우 함수 return 타입에서 성공했을 때의 타입과 실패했을 때의 타입이 모두 존재하게 되는데, 이를 통해 예외 처리를 강제할 수 있는 것과, 인텔리센스를 이용해서 자동 완성을 할 수 있다는 장점이 존재합니다.

이를 위해 다음과 같은 패턴을 사용하기도 합니다.

const func = () => {
	//에러인 경우
  	return {
    	isError: true,
      	...
    }
      
    // 정상인 경우
    return ...
}

const a = func()
if (a.isError) {
	...
} else {
	...
}

이 방법은 제가 개인적으로 알게된 내용이라 부족할 수 있으나, 이와 비슷한 예외 처리 방법인 Result<T, E> 이 있습니다. 타입스크립트 내용은 아니고, 러스트의 예외 처리 방법입니다만 꽤나 흥미로우니 한 번쯤 읽어보셔도 좋을 것 같습니다.

책에서 Option 타입에 대해서 자세히 설명하긴 했지만, 타입스크립트에서 별도의 라이브러리를 써야 사용할 수 있고, 결정적으로 제가 이해를 못해서 넘어가도록 하겠습니다.

😎 느낀 점

타입스크립트를 몇 달 정도 사용하면서 이전 타입스크립트 어느 정도 알고있다! 라고 자신하고 있었는데 이 책을 1/3 정도 읽고 나서 이 생각이 와장창 무너졌습니다. 하긴 고등학생이 알긴 뭘알ㅇ.. 정말 프로그래밍은 파고 들어갈 수록 흥미로운 부분이 많을 것 같습니다.

책 내용을 바로 쓴 것이 아니라, 책을 읽으면서 쓴 메모를 바탕으로 쓴 지라 실제 책의 내용의 반도 다루지 못했습니다. 포스트에서 다룬 내용 외에도 흥미로운 내용이 많으니 여러분도 꼭 읽어보셨으면 좋겠습니다.


2022.09.28, 지구과학2 시간에 물리학실에서...
(지구과학실은 2학년에게 뺏겼습니다...😵‍💫)

디스코드 서버

https://discord.gg/BWutkCwtsy

Opize 개발이야기와 관련한 이야기를 할 수 있는 디스코드 서버를 만들었습니다. 작성한 포스트에 대한 이야기도 하고, 포스트를 작성하는 중에는 디스코드에서 라이브를 열고 있으니 와서 구경도 하고 같이 이야기도 나누었으면 합니다!

Opize 개발 이야기 시리즈

제 포스트는 다음 시리즈들로 구성됩니다! 아직 작성 초기라 글이 없는 시리즈로 있으니 양해부탁드립니다...

기획 시리즈
디자인 시리즈
디자인 시리즈
프론트엔드 시리즈
백엔드 시리즈
클라이언트 라이브러리 시리즈
개발 시리즈
운영 시리즈

profile
새로운 상상을 하고, 상상을 현실로 만드는 개발자

0개의 댓글