타입 추론에서 발생할 수 있는 몇 가지 문제와 그 해법을 알아보자.
[ 알 수 있는 것 ]
- ts가 어떻게 타입을 추론하는지
- 언제 타입 선언을 작성해야 하는지
- 타입 추론이 가능해도 명시적으로 타입 선언을 작성해야 하는 상황은 언제인지
item 19
- 타입 추론이 된다면 명시적 타입 구문은 필요하지 않음.
- 가독성을 해치고 추후 정의한 타입을 변경할 때 자동으로 추론된 타입은 역시나 자동으로 타입을 입력하지만 하드하게 적어주면 오류를 내어 다시 하드하게 바꿔줘야 하기 때문.
- 비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 함.
- 일반적으로 함수의 매개 변수는 타입 추론이 되지 않지만 기본 값이 있을 경우엔 타입 추론이 되기 때문에 추가로 타입을 지정해줄 필요가 없음.
- 타입 정보가 있는 라이브러리에서, 콜백 함수의 매개변수 타입은 자동으로 추론됨.
// Don't do this: app.get('/health', (request: express.Request, response: express.Response) => { response.send('OK'); }); // Do this: app.get('/health', (request, response) => { response.send('OK'); });
- 그럼에도 불구하고 타입을 명시하고 싶은 경우
- 객체 리터럴을 정의해 잉여 속성 체크를 동작시키고 싶을 때.
- 함수의 반환 타입. 구현상의 오류를 체크하기 위함 + 구현 상의 오류가 사용자 코드의 오류로 표시되지 않음.
- eslint 규칙 중
no-inferrable-types
를 사용해 작성된 모든 타입이 정말 필요한지 확인할 수 있음.
item 20
- let 등으로 선언한 변수의 값은 바뀔 수 있지만 그 타입은 보통 바뀌지 않음.
- ex) let a = ‘123’으로 선언 후 a=123로 다시 선언했을 때 타입 추론은 여전히 string임.
- 해결 방법
- 유니온 타입으로 범위 확장하기 ex) string | number 이 방법은 앞으로의 더 많은 문제를 일으킬 수 있음.
- 다른 타입에는 다른 변수 사용하기 scope가 달라 같은 변수명이어도 shadowed 변수라면 다른 변수를 사용하지 않아도 됨.
const id = "12-34-56"; fetchProduct(id); { const id = 123456; // OK fetchProductBySerialNumber(id); // OK }
item 21
- 타입스크립트는 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추하는데 이를
넓히기
라고 함.
- 넓히기로 인해 오류가 발생되면, 명시적 타입 구문 또는 const 단언문을 추가하는 것을 고려하자.
const v1={ x: 1, y: 2, }; // Type is { x: number; y: number; } const v2={ x: 1 as const, y: 2, }; // Type is { x: 1; y: number; } const v3={ x: 1, y: 2, } as const; // Type is { readonly x: 1; readonly y: 2; } const a1 = [1, 2, 3]; // Type is number[] const a2 = [1, 2, 3] as const; // Type is readonly [1, 2, 3]
item 22
- 넓히기와 반대로 ts에는
좁히기
라는 개념이 있음.
- 예시
if(el) { … }
와 같이 null 값이 들어올 수도 있던 el에 if문을 통과시켜줌으로써 null 타입을 제거.Array.isArray(terms) ? terms : [terms]
처럼 삼항 연산자를 통해서 무조건 string[] 타입이 오도록 좁힐 수 있음.
- 다만 섣불리 타입을 판단하는만큼 ts는 typeof null이 ‘object’이기 때문에
if
(
typeof
el === 'object') { ... }
와 같은 경우 실수를 유발하기 쉬움.
- 사용자 정의 타입 가드.
- ts가 타입을 식별하지 못할 때, 식별을 돕기 위한 커스텀 함수.
function isInputElement(el: HTMLElement): el is HTMLInputElement { return 'value' in el; } function getElementContent(el: HTMLElement) { if (isInputElement(el)) { el; // Type is HTMLInputElement return el.value; } el; // Type is HTMLElement return el.textContent; }
item 23
- 객체 전개 연산자를 사용하여 타입 정의를 한꺼번에 할 수 있음.
const pt = {}; pt.x = 3; // ~ Property 'x' does not exist on type '{}' pt.y = 4; // ~ Property 'y' does not exist on type '{}' // ts가 새로운 타입을 추론할 수 있게 함. interface Point { x: number; y: number; } const pt0 = {}; const pt1 = {...pt0, x: 3}; const pt: Point = {...pt1, y: 4}; // OK // 삼항 연산자도 사용 가능. declare let hasMiddle: boolean; const firstLast = {first: 'Harry', last: 'Truman'}; const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
- 만약 삼항 연산자를 사용할 경우 타입이 존재하지 않을 수 있음. ex) ‘middle’ 속성이 없습니다.
- 삼항 연산자로 표현하려면 헬퍼함수를 사용하자.
function addOptional<T extends object, U extends object>( a: T, b: U | null ): T & Partial<U> { return {...a, ...b}; }
item 24
- 별칭을 일관되게 사용하자.
- 비구조화 문법으로 가능
const
{bbox} = polygon;
- 함수 호출이 객체 속성의 타입 정제를 무효화할 수 있다는 점을 주의하자.
function fn(p: Polygon) { /* ... */ } polygon.bbox // Type is BoundingBox | undefined if (polygon.bbox) { polygon.bbox // Type is BoundingBox fn(polygon); polygon.bbox // Type is still BoundingBox }
- 위 예시에서 fn(polygon) 호출은 polygon.bbox를 제거할 가능성이 있으므로 타입에 undefined를 포함하는 것이 좋음.
item 25
- 어떤 함수가 프로미스를 반환한다면 async로 선언하는 것이 좋음.
item 26
- ts는 일반적으로 값이 처음 등장할 때 타입을 결정함.
type Language = 'JavaScript' | 'TypeScript' | 'Python'; function setLanguage(language: Language) { /* ... */ } setLanguage('JavaScript'); // OK let language = 'JavaScript'; setLanguage(language); // ~~~~~~~~ Argument of type 'string' is not assignable // to parameter of type 'Language' // 해결 방법 // 1. 타입 선언 let language: Language = 'JavaScript'; setLanguage(language); // OK // 2. 상수 단언 const language = 'JavaScript'; setLanguage(language); // OK // 첫 번째 방식이 더 나은듯? 다음과 같은 에러를 보자. function panTo(where: [number, number]) { /* ... */ } panTo([10, 20]); // OK const loc = [10, 20]; panTo(loc); // ~~~ Argument of type 'number[]' is not assignable to // parameter of type '[number, number]' // 해결 const loc: [number, number] = [10, 20]; //or (아마 아래같은 경우는 타입을 직접 지정할 수 없는 경우에 사용하는 걸로 추정) function panTo(where: readonly [number, number]) { /* ... */ } const loc = [10, 20] as const; panTo(loc); // OK
item 27
- 함수형 기법과 라이브러리를 쓰면 ts 오류를 줄일 수 있음.
const csvData = "..."; const rawRows = csvData.split('\n'); const headers = rawRows[0].split(','); const rows = rawRows.slice(1).map(rowStr => { const row = {}; rowStr.split(',').forEach((val, j) => { row[headers[j]] = val; }); return row; }); // ~~~~~~~~~~~~~~~ No index signature with a parameter of // type 'string' was found on type '{}' // 함수형 const rows = rawRows.slice(1) .map(rowStr => rowStr.split(',').reduce( (row, val, i) => (row[headers[i]] = val, row), // ~~~~~~~~~~~~~~~ No index signature with a parameter of // type 'string' was found on type '{}' {})); // lodash import _ from 'lodash'; const rows = rawRows.slice(1) .map(rowStr => _.zipObject(headers, rowStr.split(',')));
- 그 외 Object.values를 map하는 것보다 flat()을, lodash로 간편하게 하는 예제 등이 있음.