typescript keyof

example.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export type User = {
name: string;
email: string;
};

const user: User = {
name: 'chinsung',
email: 'chin_sung@naver.com',
};

// keyof User === 'name' | 'email'
const updateUser = (name: keyof User, value: string) => {
return {
...user,
[name]: value,
};
};

updateUser(''); // wow 자동완성!!
  • 나이수

참고

typescript is keyword

1
2
3
4
5
function isValidPostAttributes(
attributes: any
): attributes is PostMarkdownAttributes {
return attributes?.title;
}
  • remix 튜토리얼하다가 요상한 문법을 보았다
  • any같은 . 찍으면 자동완성 안뜨는 타입들을 특정 타입으로 좁혀줘서, 자동완성 지원도 받을 수 있다
example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function isNumber(x: any): x is number {
return typeof x === 'number';
}

function isString(x: any): x is string {
return typeof x === 'string';
}

function hello(value: any) {
if (isNumber(value)) {
return value.toFixed();
}
if (isString(value)) {
return value.length;
}
throw new Error(`hello error`);
}
  • any로 받았는데, 타입 가드를 통과하면
  • 해당 타입처럼 다룰 수 있다
  • as로 강제 캐스팅하여 사용하지 않아도 된다!

참고

ts enum spread

example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum TestStatus {
PENDING,
ACCEPTED,
WRONG_ANSWER,
}
console.log(Object.values(TestStatus));
// [ 'PENDING', 'ACCEPTED', 'WRONG_ANSWER', 0, 1, 2 ]

export enum Actor {
USER = 'USER',
HOST = 'HOST',
ADMIN = 'ADMIN',
}

console.log(Object.values(Actor));
// [ 'USER', 'HOST', 'ADMIN' ]
  • 일반 Numeric enums의 경우 멤버 이름과 값이 나온다
  • String enums의 경우 값만 나온다
  • es2017 이상에서 사용할 수 있다

참고

tsconfig baseUrl eslint

tsconfig에 baseUrl을 ./src로 설정했다
그런데 eslint가 못 알아먹는다
알아먹도록 해보자

상황

  • 타입스크립트 프로젝트에서 npx eslint --init을 통해 eslint 환경을 구성했다
  • 그런데 tsconfig에 설정한 baseUrl을 eslint가 알아먹지 못했다

eslint-import-resolver-typescript 설치

terminal
1
yarn add -D eslint-import-resolver-typescript

eslintrc 수정

.eslintrc.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
// ...
"parserOptions": {
// ...
"project": "./tsconfig.json"
},
// ...
// https://github.com/import-js/eslint-plugin-import/issues/1485#issuecomment-535351922
"settings": {
"import/resolver": {
"typescript": {}
}
}
}
  • parserOptions.projectsettings.import/resolver.typescript에 위 내용처럼 추가한다
  • eslint config를 수정하면 항상 ESLint: Restart ESLint Server 또는 Developer: Reload Window을 꼭 해주자

참고

typescript에서 jest 쓰기

Jest encountered an unexpected token
SyntaxError: Cannot use import statement outside a module

따라하기

terminal
1
2
3
4
5
6
7
# jest 설치
yarn add --dev jest
yarn add --dev @types/jest

# typescript에서 jest 돌리기 위한 추가 종속성 설치
yarn add --dev @babel/preset-typescript
yarn add --dev babel-jest @babel/core @babel/preset-env
  • 종속성을 설치한다
babel.config.js
1
2
3
4
5
6
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};
  • babel.config.js 를 추가한다

eslint를 같이 사용하고 있다면

eslintrc.json
1
2
3
4
5
6
7
{
"env": {
// ...
"jest": true
}
// ...
}
  • eslintrc 에 env.jest를 true로 해준다

참고

typescript Partial

자주 쓰는 타입 유틸 ; Partial, Pick, Omit, Record

Partial

  • Partial을 사용해서 각 필드를 옵셔널로 만들 수 있다
example.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Todo = {
title: string;
description: string;
};

const updateTodo = (todo: Todo, fieldsToUpdats: Partial<Todo>) => {
return { ...todo, ...fieldsToUpdats };
};

const todo1: Todo = {
title: 'be super',
description: 'power overwhelming',
};

const todo2 = updateTodo(todo1, { description: 'show me the money' });

console.log(todo2);

Pick

Pick.ts
1
2
3
4
5
6
7
type Todo = {
title: string;
description: string;
time: string;
};

type Todo2 = Pick<Todo, 'title' | 'time'>;

참고

setTimeout typescript

1
2
class Dice {
sto: ReturnType<typeof setTimeout>;
  • typescript로 웹 스크립트를 짜고 있다
  • clearTimeout을 사용하기 위해 멤버 변수 sto를 넣었다
  • 근데 setTimeout의 타입이 뭘까?
  • 에디터에서 NodeJS.Timeout라고 알려주긴 하는데..
  • 내 런타임을 노드가 아니라 브라우저다
  • ReturnType<fn>을 사용하면 브라우저든 SSR이든 걱정 없다고 한다

참고

내가 타입스크립트를 쓰는 이유

타입스크립트 입문 전

  • C, C++, JAVA를 학교 수업으로 들었다
  • 3,4 학년 때는 웹 프로그래밍을 많이 하면서 백엔드는 nodejs, 프론트는 ejs를 했었다
  • nodejs 위에서 js를 쓰면서 이것저것 쓰면서 익숙해지고 있었다
  • 자바스크립트는 공부해서 했기보다는 그냥 막히는 부분마다 검색해서 해결했었다
  • for문도 그냥 array api(foreach, map, reduce) 안 쓰고 for(;;)로 다 짜고..
  • 그당시 내가 느끼기로 자바스크립트는 C나 JAVA에 없는 괴랄한 문법이 많이 있었다
  • 그런거에 흥미를 가지면서 for(;;) 쓰는거 그만하고 js스러운 코드를 짜기 시작했었던 같다
  • 또 js는 여태껏 배워온 C나 JAVA와 다르게 타입에 전혀 얽매이지 않고, 웬만하면 에러없이 동작해서 엄청 편했다

타입스크립트 입문 동기

  • ts를 한번 쓰면 못 빠져나온다는 조언을 들음
  • (그만큼 좋다)

타입스크립트 첫인상

  • 기존에 배웠던 엄격한 언어들과는 다르게 js는 엄청 유연한? 언어였다
  • 나는 이런 특성이 편하게 다가왔다
  • 타입 때문에 골머리 아프지 않아도 돼서 뭔가 해방된 느낌이 들었기 때문이다
  • 그런데 타입스크립트라고 자바스크립트에서 굳이 타입을 붙여 써서 뭐가 좋아지는지
  • 왜 다시 역행하는지에 대해 약간 의문이 있었다
  • 실제로 시작해서도 그간 타입을 명시하지 않았던 편안함을 잃은 기분이었다
  • 타입스크립트를 쓰면…

1 + ‘1’
// ‘11’

  • 숫자+숫자로 예상했는데 실제론 숫자+문자열하는 일은 안 겪어되는 것부터 해서
  • 실행 전에 에러를 잡아 준다는데,,
  • 그거야 처음에 잘 짜면 되는 것을,, 이때는 공감하지 못했다

내가 생각하는 타입스크립트 장점

7개월 차 (20년 9월 - 21년 4월)

  • 타입스크립트는 짱이다
  • 위에서 말한 타입스크립트를 쓰는 일반적인 장점이 있지만,
  • 나는 이 장점 다 필요 없고 자동완성이 100% 보장된다는 점에서 타입스크립트를 좋아한다
  • 예를 들면 그냥 js를 쓸 때는 배열 변수에 . 찍어도 array api 자동완성이 안된다

  • 위가 js고 아래가 ts다
  • 위는 그냥 arr 변수가 배열이든 아니든 그냥 스니펫만 제공하는 반면
  • 아래 ts의 경우 배열 타입에 맞춰서 그에 맞는 array api를 자동완성해주는 모습이다
  • 이 정도까지는 그냥 코드 스니펫으로 해결할 수 있는 수준이지만
  • 이런 api 자동완성은 기본이고, 어떤 클래스나 객체를 생성해서 하위에 멤버 변수가 있을 때

  • .만 찍으면 전부 자동완성 해준다. 오타로 인해 삽질할 필요가 전혀없는거다
  • 나는 코드 쓸 때 쉬프트 누르는 것을 별로 안 좋아한다. 쉬프트를 누르면 순간 느려지고 오타도 생기기도 하고.. 답답해진다
  • c나 java를 할 때도 일단 소문자로 쓰고 나중에 rename 기능을 통해 카멜 케이스로 변경할 만큼..
  • ts를 쓰면 이런 고민이 필요 없는 게 최초 1회만 잘 작성해두면 ctrl space만으로 코드를 작성할 수 있다 ㅋㅋㅋ
  • 또 장점이라면 내가 잘못된 코드를 작성했을 때 실행도 시켜주지 않기 때문에 모르고 지나갈 수 없다
  • 오류가 생기면 어디서 오류가 생겼는지 확실히 알 수 있다!
  • 결론! 자동완성에 한번 빠지면 헤어 나올 수 없다
  • 아니 이것도 자동완성이 된다고? 하는 곳까지 진짜 다 된다
  • ts하다가 js가면 한숨이 나올 것이다

타입스크립트 더 고수가 되고 생각나면 추가해야지..

Map을 포함한 Object JSON stringify

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface MyType {
id: number;
cover: string;
outComes: Map<number, string>;
questions: Map<number, Map<number, string>>;
}

const stringify = (object: any) => {
for (const eachIdx in object) {
if (object[eachIdx] instanceof Map) {
object[eachIdx] = Array.from(object[eachIdx]);
stringify(object);
} else if (typeof object[eachIdx] == 'object') stringify(object[eachIdx]);
}
return JSON.stringify(object, null, 2);
};

const jsonString2ObjectWithMap = <ReturnType>(
jsonString: string
): ReturnType => {
const object = JSON.parse(jsonString);
console.log(`-------------변환전`);
console.log(object);

const jsonstringToObject = (object) => {
for (const eachIdx in object) {
if (
object[eachIdx] instanceof Array &&
object[eachIdx].length > 0 &&
object[eachIdx][0].constructor === Array
) {
object[eachIdx] = new Map(object[eachIdx]);
jsonstringToObject(object);
} else if (typeof object[eachIdx] == 'object')
jsonstringToObject(object[eachIdx]);
}

return object;
};

console.log(`-------------변환후`);
const result = jsonstringToObject(object);

console.log(result);

return result;
};
실행
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
29
30
31
32
33
const myObject: MyType = {
id: 30,
cover: 'cover',
outComes: new Map([
[0, 'o1'],
[1, 'o2'],
]),
questions: new Map([
[
0,
new Map([
[1, 'answer1'],
[2, 'ansewr2'],
]),
],
[
1,
new Map([
[1, 'ansewr1'],
[2, 'ansewr2'],
]),
],
[2, new Map([])],
]),
};

console.log(`-----------------map을 포함한 오브젝트 json stringify`);
const rst = stringify(myObject);
console.log(rst);
console.log(typeof rst);

console.log(`-----------------다시 오브젝트화`);
const result = jsonString2ObjectWithMap<MyType>(rst);
output
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-----------------map을 포함한 오브젝트 json stringify
{
"id": 30,
"cover": "cover",
"outComes": [
[
0,
"o1"
],
[
1,
"o2"
]
],
"questions": [
[
0,
[
[
1,
"answer1"
],
[
2,
"ansewr2"
]
]
],
[
1,
[
[
1,
"ansewr1"
],
[
2,
"ansewr2"
]
]
],
[
2,
[]
]
]
}
string
-----------------다시 오브젝트화
-------------변환전
{
id: 30,
cover: 'cover',
outComes: [ [ 0, 'o1' ], [ 1, 'o2' ] ],
questions: [ [ 0, [Array] ], [ 1, [Array] ], [ 2, [] ] ]
}
-------------변환후
{
id: 30,
cover: 'cover',
outComes: Map { 0 => 'o1', 1 => 'o2' },
questions: Map { 0 => [ [Array], [Array] ], 1 => [ [Array], [Array] ], 2 => [] }
}

상황

  • 서버로 json 형태로 값을 전달하고 싶은데…
  • Map은 json stringify로 변환되지 않는다
  • 변환하기 위해서는 한번 Array로 변환한 다음에야 가능했다
  • 어떤 객체에 맵이 중첩으로 사용된 경우 수동으로 바꿔주는 게 귀찮다

해결

stringify.ts
1
2
3
4
5
6
7
8
9
const stringify = (object: any) => {
for (const eachIdx in object) {
if (object[eachIdx] instanceof Map) {
object[eachIdx] = Array.from(object[eachIdx]);
stringify(object);
} else if (typeof object[eachIdx] == 'object') stringify(object[eachIdx]);
}
return JSON.stringify(object, null, 2);
};
  • 객체 안 멤버들을 하나씩 돌아가면서 Map이거나 오브젝트인지 확인한다
  • Map이면은 JSON stringify 할 수 있는 Array로 변환한다
  • 오브젝트이면 중첩되어있는 Map을 찾기 위해 재귀적으로 반복한다

사용법

  • myObject와 같은 중첩 map을 포함하고 있고,
  • 포함되어있는 Map이 Map<number,string> 일 경우에만 때만 테스트해보아서, 더 다양한 경우에도 동작할지는 모르겠다
  • input : { a:… } 처럼 오브젝트를 넣어야 한다
  • console.log 찍어봤을 때 { }로 묶여있는…
  • 만약에 그냥 Map인 경우에는 이런 식으로 { a: new Map() } 한번 감싸주면 된다

repo

js bind

  • typescript 코드다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface myType {
name: string;
age: number;
}

const me: myType = {
name: 'name',
age: 99,
};

[1, 2, 3, 4, 5].forEach(function (this: myType, value, index) {
console.log(this);
console.log(value, index);
}, me);
// 0 1 { name: 'name', age: 11 }
// 1 2 { name: 'name', age: 11 }
// 2 3 { name: 'name', age: 11 }
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
29
30
31
32
33
34
35
36
37
38
39
40
41
class MyClass {
me: Person;

constructor(person: Person) {
this.me = person;
}

someFunction() {
[1, 2, 3].forEach(function (value) {
console.log(value, this);
});
}

someFunctionBindThis() {
[1, 2, 3, 4].forEach(function (this: MyClass, value) {
console.log(value, this);
}, this);
}

someFunctionWithArrow() {
[1, 2, 3].forEach((value) => {
console.log(value, this);
});
}
}

const myclass = new MyClass(me);
myclass.someFunction();
// 1 undefined
// 2 undefined
// 3 undefined

myclass.someFunctionBindThis();
// 1 MyClass { me: { name: 'name', age: 11 } }
// 2 MyClass { me: { name: 'name', age: 11 } }
// 3 MyClass { me: { name: 'name', age: 11 } }

myclass.someFunctionWithArrow();
// 1 MyClass { me: { name: 'name', age: 11 } }
// 2 MyClass { me: { name: 'name', age: 11 } }
// 3 MyClass { me: { name: 'name', age: 11 } }
  • bind 함수를 배워보았다
  • bind를 배우면서 functionarrow function 차이를 실감하게 되었다
  • arrow function은 this를 바인딩하지 않는다
  • 고로 arrow function은 bind를 사용할 수 없다
  • 이 함수 표현은 메서드 함수가 아닌 곳에 적합하다

참고