.
본문 바로가기
TypeScript

우아한 타입스크립트 실전 코드 작성하기(2)

by 와칸다개발자 2022. 6. 8.

오버로딩 흉내내기

 

ts,js에서는 오버로딩을 지원하지 않는다. 

 

오버로딩이란 같은 메소드 명이라도 변수의 타입, 변수 길이에 따라 다르게 구현하는 것

 

일단 코드를 보자

 

function shuffle (value: string | any[]): string | any[] {
  if (typeof value === 'string') {
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  }
  return value.sort(() => Math.random() - 0.5);
}

const a1 = shuffle('hi mart') // string | any[]
const a2 = shuffle(['a','b','c','d','e']);  // string | any[]
const a3 = shuffle([1,2,3,4,5]);  // string | any[]

이 shuffle 함수는 string | 배열을 반환하는 함수이다.

 

오버로딩을 활용하여 좀 더 엄격한 타입을 리턴받을 수 있다.

 

function shuffle (value: string):string;
function shuffle<T>(value:T[]):T[];
function shuffle (value: string | any[]): string | any[] {
  if (typeof value === 'string') {
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  }
  return value.sort(() => Math.random() - 0.5);
}

const a1 = shuffle('hi mart') // string
const a2 = shuffle(['a','b','c','d','e']);  // string[]
const a3 = shuffle([1,2,3,4,5]);  // number[]

보통 다른 언어라면 오버로딩을 구현할 때 각각 메소드마다 다른 로직을 구현해줘야 한다.

 

하지만 ts에서는 저렇게 위에 함수를 선언해주기만 하면 된다.

 

참고로 함수 선언식에서만 오버로딩이 가능하고 화살표 함수로 작성된 형태는 불가능하다.

 

클래스에서 오버로딩은 다음과 같이 작성할 수 있다.

 

class Modal {
  public openComponent(id:string, components: string[]): void;
  public openComponent(components: string[]): void;
  public openComponent(idOrComponents: string | string[], components?: string[]): void {

    if(typeof idOrComponents === 'string'){
      if(components !== undefined){
        idOrComponents; // string
        components; //  string[]
      }
    }
    else if(typeof idOrComponents === 'object'){
      if(components !== undefined){
        idOrComponents; // string []
        components; //  string[]
      }
    }

    if(components === undefined){
      idOrComponents;   // string | string[]
    }
  }
}

const modal = new Modal();
modal.openComponent('id',['id1', 'id2']);

openComponent 라는 함수를 오버로딩 했다. 구현 로직에서 

 

첫번째 파라미터인 idOrComponents 는 반드시 위 오버로딩한 함수 선언부 첫번쨰 파라미터의 타입들을

 

유니온 타입으로 다 받아들여야 한다.

 

안그러면 오류

 

Mapped Type

 

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

type ReadonlyPerson = Readonly<Person>;

const person: ReadonlyPerson = Object.freeze<Person>({
  name: 'king',
  age:2
});

person.name = '.';	// error
person.age = 23;	// error

 

Readonly<generic> 타입은 제너릭에 들어온 타입을 모두 readonly 키워드로 바꿔준다. 즉

 

다음과 동일하다.

 

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

 

다음 코드를 보자.

 

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

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
}

type Stringify<T> = {
  [P in keyof T]: string;
}

type PartialNullablePerson = Partial<Nullable<Stringify<Person>>>;
let p: PartialNullablePerson;
p = { name: 'king', age: '2'};
p = { name: 'king' };
p = { name: undefined , age: null};

 

먼저 Nullable 을 살펴보자 제너릭을 받아 제너릭의 키 속성을 순회하면서 null 과 함께 유니온 타입으로 명시해준다.

 

즉 기존 제너릭 객체 타입에 null 타입을 추가해주는 것.

 

Stringify 는 말그대로 제너릭으로 들어온 속성 타입을 다 string으로 바꿔주는 것이다.

 

type PartialNullablePerson = Partial<Nullable<Stringify<Person>>>;

 

그럼 위 타입은 설명하자면 제너릭으로 들어온 타입을 모두 string 으로 변환하고 null 과 유니온 타입이 되고

 

모든 속성이 옵셔너블 해진다. 다음과 같다.

 

{
	name? : string | null | undefined;
	age? : string | null | undefined;
}

 

다음은 공식 DOC에서 내장되어있는 유틸리티 타입이다 내부 구현은 다음과 같이 이루어져 있다.

 

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

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

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

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
    [P in K]: T;

신기하다.

 

readonly <T>

 

interface Book {
  title: string;
  author: string;
}

interface RootState {
  book: {
    books:Book[];
  }
}

type ReadonlyRootState = Readonly<RootState>;
let state: ReadonlyRootState = {} as ReadonlyRootState;
const book1 = state.book.books[0];
book1.title = 'ssssss';			// 띠용? set이 가능해짐

 

위 코드는 RootState 인터페이스의 모든 속성을 읽기 전용으로 바꾸었지만 

book1.title 의 set이 가능해지는 상황이다. 

 

당연한것이 RootState의 book 프로퍼티만 읽기 전용일뿐 book 의 프로퍼티는 읽기 전용이 아니기 때문이다.

그래서 재귀적으로 객체의 모든 속성을 읽기 전용으로 만들어야 한다.

 

공식 문서를 뒤져본 결과 이러한 유틸리티 타입은 아직 없는 것 같다. 직접 구현해야 한다.

 

DeepReadonly<T>

 

type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonlyObject<E>>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

type DeepReadonlyObject<T> = {
  readonly [k in keyof T]: DeepReadonly<T[k]>;
}

type DeepReadonlyRootState = DeepReadonly<RootState>;
let state2: DeepReadonlyRootState = {} as DeepReadonlyRootState;
const book2 = state2.book.books[0];
book2.title = 'ssssss';   // error 더 이상 set할 수 없음

 

아주 읽기 무서운 코드가 나왔다. 차근차근 살펴보자.

 

먼저 DeepReadonly<T> 타입은 제너릭으로 들어온 T가 배열 타입이라면 배열의 원소 타입을 E로 추론하고

ReadonlyArray로 감싸고 그 안에 들어 있는 배열의 원소를 DeepReadonlyObject로 만들어 준다.

 

제너릭 타입이 객체라면 역시 DeepReadonlyObject로 감싸서 만들어 준다.

 

type DeepReadonlyObject<T> = {
  readonly [k in keyof T]: DeepReadonly<T[k]>;
}

 

제너릭타입 T의 키 프로퍼티를 순회하면서 T[k]의 속성을 DeepReadonly로 감싸주면 재귀적으로 타고 타고 들어가

읽기 전용으로 바꾸어 준다. 실전으로 많은 코드를 작성해 봐야 겠다...

 

다음 시간에 계속..

'TypeScript' 카테고리의 다른 글

type challange <Easy>  (0) 2022.11.03
우아한 타입스크립트 실전 코드 작성하기(1)  (0) 2022.05.26
여러가지 형태의 타입가드  (0) 2022.05.09
Type vs Interface  (0) 2021.12.05
abstract 와 interface 차이점  (0) 2021.11.08

댓글