Typescript | JS는 정말 좋은 아이였구나(feat. Indexable)

신세원·2020년 12월 25일
3
post-thumbnail

프로젝트를 진행하면서, javascirpt가 그동안 정말 유연하고 좋았던 아이였다는걸 깨달았다.

#Intro

IndexAble을 설명하기 앞서 간단한 코드카타를 하고 넘어가자.

문제) ['2020', 'Jan', '23'] 의 배열을 '2020-01-23'로 만드시오.


const calenderList = {
  Jan: '01',
  Feb: '02',
  Mar: '03',
  Apr: '04',
  May: '05',
  Jun: '06',
  Jul: '07',
  Aug: '08',
  Sep: '09',
  Oct: '10',
  Nov: '11',
  Dec: '12',
};

arrList=['2020','Jan','23']

  const result=arrList.map((item)=>{
    if(item in calenderList){
      return item=calenderList[item]
    }
    return item;
  })
 
console.log(result.join('-')) // '2020-01-23'

프로젝트를 진행하고 있었다. 오랜만에 로직을 짤 일이 있어서 코드카타하는 즐거운 마음으로 RunJs로 로직을 짜고 있었고 위에서 처럼 답을 얻고 프로젝트 파일로 돌아가서 적용을 하려했다 .

그러나 한가지 예상치 못한 문제가 발생되었다...

아래 변수명이 다른건 프로젝트를 진행하면서 적은 내용이기 때문에 위에 문제와 상이하다. 밑에 빨간 줄 쳐진것만 보자

저기 저 빨간줄... 문제의 원인은 'Typescript IndexAble'이었다.
Typescript는 javascript처럼 곧이 곧대로 적기만 하면 인식을 못한다.
이 부분에서 javascript가 유연한 언어이고, typescript는 정적인 언어인지 확실하게 깨달았다.
그럼 이 문제를 해결하기 위해서 어떻게 해야할까?

해결하기전에 IndexAble에 대해 알아보고 해결해보도록 하자.

# IndexAble 타입

javascript 색인의 동작 방식

타입스크립트로 들어가기전에 자바스크립트의 객체 프로퍼티를 접근하는 방법을 살펴보자.
자바스크립트는 객체의 프로퍼티에 접근을 할 때 문자열로 접근할 수 있다. [] 를 이용하여 접근이 가능하다.

// ES6
let obj = {};
obj['str'] = 'string';
console.log(obj['str']); // string

그리고 객체의 프로퍼티로 지정할 수 있다.

// ES6
let obj = {};
let foo = {};
foo[obj] = 'Key is obj';
console.log(foo[obj]); // Key is obj

자바스크립트 색인의 동작방식에 의해 객체의 색인에 접근할 때 내부적으로 toString() 메서드를 호출하여 문자열로 변형된 값을 통해 접근한다.

// ES6
let obj = {
    toString() {
        console.log('toString() called');
    }
};
let foo = {};
foo[obj] = 'Key is obj'; // toString() called
console.log(foo[obj]);
// toString() called
// Key is obj

toString() 메서드를 호출해 문자열로 바뀌는것을 콘솔로 확인하였다. 위의 예제에서는 toString() 에 대한 콘솔로그를 두번 호출하는데 그 이유는 접근할 때마다 toString() 이 호출되기 때문이다.

Typescript Indexable 사용법

자바스크립트에서 사용하듯 타입스크립트에서 객체를 하나 만들어보자.

// ES6
const obj = {
  name: '세원',
  address: '서울',
};
Object.keys(obj).forEach(key => console.log(obj[key]));

하지만 이 코드는 타입스크립트에서는 에러를 발생한다.

TS7017: Element implicitly has an 'any' type because type '{ a: string; b: string; }' has no index signature.

index signature 가 없다는 에러메세지가 보인다. 이유는, 프로퍼티에 접근할 때 어떤 타입인지 확인할 수 없어 암묵적으로 any 타입을 사용하기 때문이다.

이는 tsconfig 의 "noImplicitAny": true 이기 때문에 발생하는 에러이다.
noImplicitAnydefaulttrue 이다.

해결방법은 index signature 를 사용하면 된다. 다음 예제를 봐도록 하자.

// TS
interface IndexSignature {
    [key: string]: string;
}
const obj: IndexSignature = {
  name: '세원',
  address: '서울',
};
Object.keys(obj).forEach(key => console.log(obj[key]));

index signature 의 의미는 key 값은 string 이고 반환값도 string 이다. 라는 뜻이다.

주의점

1. index signature 의 타입은 문자열 또는 숫자만 가능하다.

// TS
// Error!
interface Interface {
    [key: boolean]: string;
}
//  TS1023: An index signature parameter type must be 'string' or 'number'

2. 문자열 색인과 숫자 색인이 모두 존재할 경우, 숫자로 된 색인의 값의 타입은 문자열로 색인 된 값 타입의 서브타입이어야 한다.

// TS
class Fruits {
    name: string;
}
class Kiwi extends Fruits {
    goldKiwi: string;
}
// Error: "문자열"로 색인을 생성하면 가끔 "Kiwi"가 생깁니다.
interface NotOkay {
    [x: number]: Fruits;
    [x: string]: Kiwi;
}

위의 코드가 에러인 이유는 처음에 이야기한 자바스크립트가 색인을 할 때 toString() 을 먼저 호출하기 때문이다.

가령, obj[1] 로 접근을 하면 우리가 기대했던 Fruits 이 값으로 나올 것 같지만 1 은 문자열 '1' 로 변환이 되기 때문에 Kiwi 가 나올수도 있다는 예제이다.

// ES6
console.log((1).toString() === '1') // true

Typescript Handbook - Interfaces 에서는 문자열 Index Signature 은 'Dictionary' 패턴을 설명하는 강력한 방법이지만, 모든 프로퍼티가 리턴 타입과 일치 해야한다고 말하고 있다.

유니온 타입을 이용한 Index Signature

// TS
interface UnionTypeSignature {
    [key: string]: number | string;
    name: string;
    age: number;
}
const person: UnionTypeSignature = {
  name: 'sewon',
  age: 20,
};
console.log(person.name); // sewon
console.log(person.age); // 20
console.log(person['name']); // sewon
console.log(person['age']); // 20

유니온 타입을 이용하려면 인덱서는 그 아래 프로퍼티의 속성들을 모두 가지고 있어야한다.

제한된 리터럴문자열

매핑된 유형을 사용해 index signature 가 문자열 조합의 구성원이어야 사용할 수 있게끔 제약할 수 있다.

// TS
type Index = 'a' | 'b' | 'c';
type FromIndex = {
    [k in Index]?: number;
}
const good: FromIndex = {
    a: 1,
    b: 2,
    c: 3,
};
/* TS2322: Type '{ b: number; c: number; d: number; }' is not assignable to type 'FromIndex'.
Object literal may only specify known properties, and 'd' does not exist in type 'FromIndex'. */
const bad: FromIndex = {
    b: 2,
    c: 3,
    d: 4, // Error
};
// d 속성이 없음

중첩된 Index Signature

// TS
interface NestedCSS {
    color?: string;
    [selector: string]: string | NestedCSS;
}
const example: NestedCSS = {
    color: 'black',
    '.subclass': {
        color: 'white',
    },
};

이렇게 했을 경우 다음과 같은 오타는 잡지 못합니다.

// TS
const failsSiently: NestedCSS = {
    colour: 'gold',
};

해결책은 다음과 같다. nest, children, subnodes 등등과 같은 이름을 갖는 프로퍼티를 만들고 그 안에 내장시킨다.

// TS
interface NestedCSS {
    color?: string;
    nest?: {
        [selector: string]: NestedCSS;
    };
}
const example: NestedCSS = {
    color: 'black',
    nest: {
        '.subclass': {
            color: 'white',
        }
    }
};

이제 다음과 같은 코드는 에러를 뱉는다.

// TS
const failsSiently: NestedCSS = {
    colour: 'gold',
};
/*
TS2322: Type '{ colour: string; }' is not assignable to type 'NestedCSS'.
 Object literal may only specify known properties, but 'colour' does not exist in type 'NestedCSS'. Did you mean to write 'color'?
*/

처음 나왔던 문제 풀어보기🔥

그렇다면 처음에 나왔던 문제를 지금까지 배운 방법으로 어떻게 풀 수 있을까?

정답은 다음과 같다.

1번 답안) Indexable 사용

interface IcalenderList{
  [key:string]:string;
}

const calenderList:IcalenderList= {
  Jan: '01',
  Feb: '02',
  Mar: '03',
  Apr: '04',
  May: '05',
  Jun: '06',
  Jul: '07',
  Aug: '08',
  Sep: '09',
  Oct: '10',
  Nov: '11',
  Dec: '12',
};

arrList=['2020','Jan','23']

  const result=arrList.map((item)=>{
    if(item in calenderList){
      return item=calenderList[item]
    }
    return item;
  })
 
console.log(result.join('-')) // '2020-01-23'

아래와 같이도 풀 수 있다.

2번 답안) keyof,typeof 사용


const calenderList= {
  Jan: '01',
  Feb: '02',
  Mar: '03',
  Apr: '04',
  May: '05',
  Jun: '06',
  Jul: '07',
  Aug: '08',
  Sep: '09',
  Oct: '10',
  Nov: '11',
  Dec: '12',
};

arrList=['2020','Jan','23']

  const result=arrList.map((item)=>{
    if(item in calenderList){
      return item=calenderList[item as keyof typeof calenderList]
    }
    return item;
  })
 
console.log(result.join('-')) // '2020-01-23'
profile
생각하는대로 살지 않으면, 사는대로 생각하게 된다.

0개의 댓글