Closed moonheekim0118 closed 1 year ago
타입스크립트 코드에서 모든 타입 정보를 제거하면 타입스크립트가 되지만, 열거형, 매개변수 속성, 트리플 슬래시 임포트, 데코레이터는 타입 정보를 제거한다고 자바스크립트가 되지는 않는다.
const enum Flaver {
VANILA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2
}
let flavor = Flaver.CHOCOLATE; // js에서는 let flavor = 1
const enum Flaver {
VANILA = 'vanila',
CHOCOLATE = 'chocolate',
STRAWBERRY = 'strawberry'
}
let flavor = Flaver.CHOCOLATE;
flavor = 'strawberry' //Type '"strawberry"' is not assignable to type 'Flaver'.
열거형 대신 리터럴 타입의 유니온을 사용하자.
자바스크립트와 호완되고 편집기에서 자동완성을 지원한다.class Person{
constructor(public name: string) {}
}
public name을 매개변수 속성이라 부른다.
클래스에 매개변수 속성만 존재한다면 클래스 대신 인터페이스로 만들고 객체 리터럴을 사용하는 것이 좋다.
const obj = {
one: 'uno',
two: 'dos',
three: 'tres'
};
for( const k in obj ){
const v = obj[k]; // obj에 인덱스 시그니처가 없기 때문에 앨리먼트는 암시적으로 'any' 타입입니다
}
k와 obj 객체의 키 타입이 서로 다르게 추론된다.
let k: keyof typeof obj;
for(k in obj){
const v = obj[k];
}
k의 타입을 구체적으로 명시해 주면 오류는 사라진다.
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for(const k in abc) {
const v = abc[k]; // 'ABC'타입에 인덱스 시그니처가 없기 때문에 앨리먼트는 암시적으로 'any' 타입입니다
}
}
const x = { a: 'a', b: 'b', c:2, d: new Date()}
foo(x) // 정상
foo 함수는 ABC 타입에 ‘할당 가능한' 어떠한 값이든 매개변수로 허용한다. 즉, ABC 타입에 할당 가능한 객체에는 a, b, c 외에 다른 속성이 존재할 수 있기 때문에, 타입스크립트는 ABC 타입의 키를 string 타입으로 선택한다.
단, keyof 키워드를 사용한 방법은 다른 문제점을 가지고 있다.
function foo(abc: ABC) {
let k: keyof ABC;
for(k in abc) { // let k: 'a' | 'b' | 'c'
const v = abc[k]; // string | number 타입
}
}
v가 string | number타입으로 한정이 되버린다. 추가적인 속성이 더해지는 경우(위의 예시처럼 d가 추가) 이는 어떠한 타입이든 될 수 있기 때문에 string | number 타입으로 추론된 것은 잘못이며 런타임 동작을 예상하기 어렵다.
단기 객체의 키와 값을 순회하고 싶다면 Object.entries를 사용하면 된다.
function foo(abc: ABC) {
for( const [k, v] of Object.entries(abc)) {
k // string
v // any
}
}
객체를 다룰 때에는 항상 프로토타입의 오염의 가능성을 염두해야 한다. for-in 구문을 사용하면, 객체의 정의에 없는 속성이 갑자기 등장할 수 있다.
for-in 루프에서 k가 string 키를 가지게 된다면 프로토타입 오염의 가능성을 의심해 봐야 한다.
keyof 선언은 상수이거나 추가적인 키 없이 정확한 타입을 원하는 경우 적적한다. Object.entries는 더욱 일반적으로 쓰이지만, 키와 값의 타입을 다루기 까다롭다.
💡 Object.entries는 타입스크립츠 3.8 기준으로 표준 함수가 이나며, tsconfig.jsondp es2017(ES8) 설정을 추가하여 사용할 수 있다.
속성에 _를 붙이는 것은 단순히 비공개라고 표시한 것 뿐이다. 일반적인 속성과 동일하게 클래스 외부로 공개되어 있다.
public, protected, private 같은 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 후에는 제거된다. 이들은 단지 컴파일 시점에만 오류를 표시해 줄 뿐이며, 런타임에는 아무런 효력이 없다. 심지어 단언문을 사용하면 타입스크립트 상태에서도 private 속성에 접근할 수 있다. 즉, 정보를 감추기 위해 private을 사용하면 안된다.
class Diary {
private secret = 'this is secret';
}
const diary = new Diary();
(diary as any).secret // 정상
자바스크립트에서 정보를 숨기기 위해 가장 효과적인 방법은 클로저를 사용하는 것이다. 그러나 클로저 방식은 동일 클래스의 개별 인스턴스 간의 속성 접근이 불가능하기 때문에 불편하다.
또 하나의 선택지로, 비공개 필드(#)을 사용할 수 있다. 비공개 필드 기능은 접두사로 #을 붙여서 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.
any 타입을 좁게 만들기
function f1(){
const x : any = expressionReturningFoo(); -> 권장하지 않음
processBar(x);
}
function f2(){
const x = expressionReturningFoo();
processBar(x as any);
}
-> any타입이 processBar의 매개변수에서만 사용된 표현식이므로 다른 코드에 영향일 미치지 않기 때문에
매개변수로 넘겨주는 곳에서만 as any로 처리하기
any는 가능한한 좁은 범위로 선언하기
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any -> 이곳에서 타입 에러가 발생했을 때
}
}
any를 사용하더라도, 그대로 사용하지 않기
function getLengthBad(array: any) -> x
getLengthBad(array: any[])
{[key: string] : any }
타입 단언문을 사용해야 한다면, 함수 내부로 숨기는 것이 좋다
function cacheLast<T extends Function>(fn: T) : T {
let lastArgs : any[]|null = null;
let lastResult: any;
return function (...args: any[]){
if(!lastArgs || !shallowEqual(lastArgs, args)){
lastResult = fn(...args);
lastArgs = args;
}
return lastResult;
} as unknown as T;
}
→ 원본 함수 T 타입과 동일한 매개변소로 호출되고 반환값 역시 예상한 결과값이 되기 떄문에, 타입 단언문을 추가해서 오류를 제거하는 것이 문제가 되지 않는다
any 타입의 진화
const result = [ ]; // any[]
result.push('a') // string[]
result.push(1) // (string | number) []
null 타입의 진화
let val = null; // 타입은 any
try {
somethingDangerous();
val = 12;
val // 타입은 number
} catch (e) {
console.warn('alas');
}
val // 타입은 number | any
function parseYAML(yaml: string) : any {
interface Book {
name: string;
author: string;
}
const book = parseYAML(`name: 'ssss' author: 'sssss'`);
함수를 호출하는 곳에서 타입을 강제해 주지 않게 되면, book은 암시적으로 타입이 any가 되고
타입 관련 오류가 발생할 확률들이 높아진다
book.title => 오류 없다고 나오지만 , 런타임시 undefined에러가 발생하게 된다
이럴 때에는, 함수의 반환 값에 unknown을 설정하게 되면 런타임 이전에 오류가 발생했다고 표시가 되게 된다
unknown의 특징
변수 선언시 unknown
어떠한 값이 있지만 그 타입을 모르는 경우에 unknown을 할당한다.
타입 단언문 대신에 instacneof 를 체크하여 unknown을 원하는 타입으로 변환할 수 있다
function processValue(val: unknown){
if(val instanceof Date){
val // 타입이 Date로 변환된다
}
}
사용자 정의 타입 가드로도 타입을 변환할 수 있다
function isBook(val: unknown): val is Book{
return(
typeof(val) === 'object' && val !== null &&
'name' in val && 'author' in val
);
}
function processValue(val: unknown){
if(isBook(val)){
val // 타입이 Book이 된다
}
}
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown을 사용하면 된다
{} 타입은 null과 undefined를 제외한 모든 값을 포함한다
obejct 타입은 모든 비기본형 타입으로 이루어진다
자바스크립트는 객체와 클래스에 임의의 속성을 추가할 수 있을 만큼 유연하다
타입스크립트는 전역객체에 임의로 속성을 추가한 값들에 대해서는 타입을 알지 못한다
해당 문제를 해결할 때에 any를 사용하면 타입 체커는 통과하지만, 타입 안정성을 상실하고 언어 서비스를 사용할 수 없다는 단점이 있다
(document as any).monkey = 'Tamarin'
document와 DOM으로 부터 임의로 부여한 속성을 사용해야하는 경우
interface Document {
monkey: string;
}
document.monkey = 'tamarin' // 정상
모듈의 관점에서 interface 보강을 사용하려면 global선언을 추가해야한다
export {};
declare global {
interface Document {
monkey: string;
}
}
더 구체적인 타입 단언문을 사용하자
interface MonkeyDocument extends Document {
monkey: string;
}
(document as MonkeyDocument).monkey = 'sdadfasd';
⇒ 타입스크립트의 경우, 개발 도구 일 뿐이고 타입정보는 런타임에 존재하지 않기 때문에 기본적으로 devDepencies에 포함되어 있다
this 값은 다이나믹하여서, 타입 관련해서 혼동이 올 수 있으므로 this와 관련해서도 타입을 명시해주어야 한다
function addKeyListner(
el: HTMLElement,
fn: (this: HTMLElement, e: KeyboardEvent) => voidl
) {
el.addEventListener('keydown', e => {
fn(el,e);
});
};
조건부 타입 사용하기
function double<T extends number | string> (
x : T
): T extends string ? string : number;
function double(x: any) { return x + x };
타입스크립트가 태동하던 2010년에 자바스크립트는 개선해야 할 부분이 많았다. (당시에는 클래스, 데코레이터, 모듈 시스템 같은 기능이 없었다.) 따라서 타입스크립트도 독립적으로 개발한 기능들을 포함시켜야 했다. 시간이 흐르며 TC39(자바스크립트를 관장하는 표준 기구)는 부족했던 점을 대부분 내장 기능으로 추가했다. 이는 타입스크립트 초기 버전에서 독립적으로 개발했던 기능과 호환성 문제를 발생시켰다. 타입스크립트 진영에서는 이에 대응하기 위한 전략을 선택해야 했다.
타입스크립트 팀은 대부분 두 번째 전략을 선택했다. TC39는 런타임 기능을 발전시키고, 타입스크립트 팀은 타입 기능만 발전시킨다는 원칙을 세우고 지켜오고 있다.
그러나 이 원칙이 세워지기 전에 이미 사용되고 있던 몇 가지 기능이 있다. 이는 타입 공간(타입스크립트)와 값 공간(자바스크립트)의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋다.
열거형(enum)
몇몇 값의 모음을 나타내기 위해 열거형을 사용한다. 단순한 값의 나열보다 실수가 적고 명확하다.
enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
let flavor = Flavor.CHOCOLATE; // 타입 Flavor
Flavor // 타입 enum Flavor, 자동완성 추천: VANILLA, CHOCOLATE, STRAWBERRY
Flavor[0] // 값은 'VANILLA'
타입스크립트 열거형의 문제점
const enum Flavor
로 바꾸면 컴파일러는 Flavor.CHOCOLATE
을 0으로 바꿔버린다. 문자열 열거형과 숫자 열거형과 전혀 다른 동작이다.preserveConstEnums
플래스를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지한다.문자형 열거형은 명목적 타이핑을 사용하기 때문에 자바스크립트와 타입스크립트에서 동작이 달라진다.
scoop('vanilla');
// 자바스크립트에서 정상
// 타입스크립트에서 오류!
// Argument of type '"vanilla"' is not assignable to parameter of type 'Flavor'.
// 타입스크립트에서는 열거형을 import하여 문자열 대신 사용해야 한다.
import { Flavor } from 'ice-cream';
scoop(Flavor.VANILLA); // 타입스크립트에서 정상
⇒ 열거형 대신 리터럴 타입의 유니온을 사용하자.
매개변수 속성
class Person {
first: string;
last: string;
constructor(public name: string) {
[this.first, this.last] = name.split(' ');
}
}
public name
은 타입스크립트에 있는 매개변수 속성이다.
매개변수 속성의 문제점
매개변수 속성과 일반 속성을 섞어서 사용하면 클래스 설계가 혼란스러워진다.
위 예시의 경우 세 가지 속성(first, last, name)이 있지만, first와 last만 속성에 나열되어 있고 name은 매개변수 속성에 있어서 일관성이 없다.
네임스페이스와 트리플 슬래시 임포트
ECMAScript 2015 이전에는 자바스크립트에 공식적인 모듈 시스템이 없었다. 그래서 각 환경은 자신만의 방식으로 모듈 시스템을 마련한다. (Node.js는 require, module.export, AMD는 define 함수와 콜백) 타입스크립트는 module
키워드와 트리플 슬래시
임포트를 사용한다.
이는 호환성을 위해 남아있을 뿐이며, 이제는 ECMAScript 2015 스타일의 import, export를 사용해야 한다.
namespace foo {
function bar() {}
}
/// <reference path="other.ts" />
foo.bar();
데코레이터
클래스, 메서드, 속성에 애너테이션(annotation)을 붙이거나 기능을 추가하는 데 사용할 수 있다. 클래스의 메서드가 호출될 때마다 로그를 남기려면 logged 애너테이션을 정의할 수 있다.
앵귤러 프레임 워크를 지원하기 위해 추가된 기능으로, tsconfig.json
에 experimentalDecorators
속성을 설정하고 사용해야 한다. 단, 표준화가 완료되지 않았으니 데코레이터가 표준이 되기 전에는 사용하지 않는 게 좋다.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@logged
greet() {
return "Hello, " + this.greeting;
}
}
function logged(target: any, name: string, descriptor: PropertyDescriptor) {
const fn = target[name];
descriptor.value = function() {
console.log(`Calling ${name}`);
return fn.apply(this, arguments);
}
}
console.log(new Greeter('Dave').greet());
아래 코드의 경우 오류가 발생한다. k의 타입은 string이고, obj 객체에는 ‘one’, ‘two, ‘three’ 세 개의 키만 존재하기 때문이다. k와 obj 객체의 키 타입은 서로 다르게 추론되어 오류가 발생한다.
const obj = {
one: 'uno',
two: 'dos',
three: 'tres'
}
for (const k in obj) {
const v = obj[k];
}
객체를 순회할 때, 키가 어떤 타입인지 정확히 파악하고 있다면 let k: keyof T
와 for-in
루프를 사용한다.
const obj = {
one: 'uno',
two: 'dos',
three: 'tres'
}
let k: keyof typeof obj;
for (k in obj) {
const v = obj[k];
}
객체는 순회하며 키와 값을 얻는 가장 일반적인 방법은 Object.entries
를 사용하는 것이다. 이는 직관적이지는 않지만, 복잡한 기교 없이 사용할 수 있다.
function foo(abe: ABC) {
for (const [k, v] of Object.entries(abc)) {
k // string 타입
v // any 타입
}
}
EventTarget
DOM 타입 중 가장 추상화된 타입.
이벤트 리스너를 추가하거나 제거하고, 이벤트를 보내는 것만 할 수 있다.
Node
Element가 아닌 Node인 경우는 텍스트 조각과 주석이 있다.
어떤 엘리먼트가 children, childNodes 속성을 가지고 있다. 이때 children은 내부의 자식 엘리먼트만 포함하는 배열과 유사한 구조인 HTMLCollection이다. 반면 childNodes는 배열과 유사한 Node 컬렉션인 NodeList로, 엘리먼트 뿐만 아니라 텍스트 조각과 주석까지 포함한다.
HTMLxxxElement
HTMLxxxElement 형태의 특정 엘리먼트들은 자신만의 고유한 속성을 가진다. 이 속성에 접근하려면 타입 정보는 실제 엘리먼트 타입이어야 하며 상당히 구체적으로 타입을 지정해야 한다.
DOM에는 타입 계층 구조가 있다. DOM 타입은 타입스크립트에서 중요한 정보이며, 브라우저 관련 프로젝트에서 타입스크립트를 사용할 때 유용하다.
Event 타입에도 계층 구조가 있다. Event 보다 MouseEvent 타입이 더 구체적이다. 핸들러를 인라인 함수로 만들면 더 많은 문맥 정보가 있어 오류를 제거할 수 있다.
public, protected, private 같은 접근 제어자는 타입스크립트 키워드라 컴파일 후에는 사라진다. 이는 컴파일 시점에만 오류를 표시할 뿐 런타임에는 아무 효력이 없다. 확실히 데이터를 감추고 싶다면 클로저를 사용해야 한다.
옛날 버전 자바스크립트 코드를 최신 버전 자바스크립트 코드로 바꾸는 작업은 타입스크립트로 전환하는 작업의 일부로 볼 수 있다. 마이그레이션을 어디서부터 해야할 지 몰라 막막하다면 최신 버전 자바스크립트로 바꾸는 작업부터 시작해보자. ES6부터 도입된 주요 기능 몇 가지를 간략히 알아보자.
ECMAScript 모듈 사용하기
import와 export를 사용하는 ECMAScript 모듈은 타입스크립트에서도 잘 동작하고 모듈 단위로 전환할 수 있게 해주기 때문에 점진적 마이그레이션이 원활해진다.
// CommonJS
// a.js
const b = require('./b');
console.log(b.name);
// b.js
const name = 'Module B';
module.exports = { name };
// ECMAScript module
// a.ts
import * as b from './b';
console.log(b.name);
// b.ts
export const name = 'Module B';
프로토타입 대신 클래스 사용하기
자바스크립트는 프로토타입 기반의 객체 모델을 사용했다. 그러나 많은 개발자가 견고하게 설계된 클래스 기반 모델을 선호하면서 class 키워드를 사용하는 클래스 기반 모델이 도입되었다.
프로토타입 기반으로 구현한 객체는 클래스 기반 객체로 바꾸면 간결하고 직관적이다. 타입스크립트 언어 서비스는 함수를 ES2015 클래스로 변환하는 기능을 제공한다.
var 대신 let/const 사용하기
let과 const는 제대로 된 블록 스코프 규칙을 가지며, 개발자들이 일반적으로 기대하는 방식으로 동작한다.
for(;;) 대신 for-of 또는 배열 메서드 사용하기
for-of 루프는 인덱스 변수를 사용하지도 않기 때문에 실수를 줄일 수 있다.
함수 표현식보다 화살표 함수 사용하기
this 키워드는 일반적인 변수들과 다른 스코프 규칙을 가진다. 일반적으로는 this가 클래스 인스턴스를 참조하는 것을 기대하지만, 다음 예제처럼 예상치 못한 결과가 나오는 경우도 있다.
class Foo {
method() {
console.log(this);
[1, 2].forEach(function(i) {
console.log(this);
});
}
}
const f = new Foo();
f.method();
// strict 모드에서 Foo, undefined, undefined를 출력한다.
// non-strict 모드에서 Foo, window, window (!)를 출력한다.
이때 화살표 함수를 사용하면 상위 스코프의 this를 유지할 수 있다.
class Foo {
method() {
console.log(this);
[1, 2].forEach(i => {
console.log(this);
});
}
}
const f = new Foo();
f.method();
// 항상 Foo, Foo, Foo를 출력한다.
단축 객체 표현과 구조 분해 할당 사용하기
함수 매개변수 기본값 사용하기
저수준 프로미스나 콜백 대신 async/await 사용하기
연관 배열에 객체 대신 Map과 Set 사용하기
타입스크립트에 use strict 넣지 않기
ES5에서는 버그가 될 수 있는 코드 패턴에 오류를 표시해주는 strict mode가 도입되었다. 코드의 제일 처음에 ‘use strict’를 넣으면 엄격 모드가 활성화된다. 그러나 타입스크립트에서 수행되는 안전성 검사가 더 엄격한 체크를 하기 때문에, 타입스크립트 코드에서 ‘use strict’는 무의미하니 제거한다.
@ts-check
와 JSDoc으로 시험해 보기@ts-check
지시자를 사용하여 타입스크립트 전환 시에 어떤 문제가 발생하는지 미리 시험해 볼 수 있다. @ts-check
지시자를 사용하여 타입 체커가 파일을 분석하고 발견된 오류를 보고하도록 지시한다.
// @ts-check
const person = { first: 'Grace', last: 'Hopper' };
2 * person.first
// 에러! 산술 연산 오른쪽은 'any', 'number', 'bigint' 또는 열거형 형식이어야 합니다.
타입스크립트와 자바스크립트가 공존하는 방법의 핵심은 allowJs 컴파일러 옵션이다.
타입스크립트 파일과 자바스크립트 파일을 서로 임포트할 수 있게 해준다. (타입스크립트는 자바스크립트의 상위 집합이다.)
대규모 마이그레이션 전에 테스트와 빌드 체인에 테스트를 적용해야 한다.
타입스크립트로 전환 시 발생할 수 있는 오류들
선언되지 않은 클래스 멤버
자바스크립트는 클래스 멤버 변수를 선언할 필요가 없지만, 타입스크립트에서는 명시적으로 선언해야 한다.
타입이 바뀌는 값
ex) 빈 객체로 변수를 선언하고 이후에 속성 추가하기
여기까지 오다니...너무 자랑스럽다 땡스기빙 타스