프로젝트를 진행하면서, javascirpt가 그동안 정말 유연하고 좋았던 아이였다는걸 깨달았다.
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
에 대해 알아보고 해결해보도록 하자.
타입스크립트로 들어가기전에 자바스크립트의 객체 프로퍼티를 접근하는 방법을 살펴보자.
자바스크립트는 객체의 프로퍼티에 접근을 할 때 문자열로 접근할 수 있다. [] 를 이용하여 접근이 가능하다.
// 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() 이 호출되기 때문이다.
자바스크립트에서 사용하듯 타입스크립트에서 객체를 하나 만들어보자.
// 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
이기 때문에 발생하는 에러이다.
noImplicitAny
의default
는true
이다.
해결방법은 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 이다.
라는 뜻이다.
// TS
// Error!
interface Interface {
[key: boolean]: string;
}
// TS1023: An index signature parameter type must be 'string' or 'number'
// 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' 패턴을 설명하는 강력한 방법이지만, 모든 프로퍼티가 리턴 타입과 일치 해야한다고 말하고 있다.
// 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 속성이 없음
// 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'?
*/
그렇다면 처음에 나왔던 문제를 지금까지 배운 방법으로 어떻게 풀 수 있을까?
정답은 다음과 같다.
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'
아래와 같이도 풀 수 있다.
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'