Effective Typescript (Day 3)

d_fe·2022년 11월 28일
post-thumbnail

item 11. 잉여 속성 체크의 한계 인지하기

타입이 명시된 변수에 객체 리터럴을 할당할 때 Ts는 해당 타입의 속성이 있는지, 그리고 '그 외의 속성은 없는지' 확인한다.

구조적 타이핑의 관점으로 생각해 보면 오류가 발생하지 않아야 한다.
임시 변수를 도입해보면 obj 객체는 Room 타입에 할당이 가능하다.

위 두 예시의 차이점은 첫째 예시에서 구조적 시스템에서 발생할 수 있는 중요한 종류의 오류를 잡을 수 있도록 '잉여 속성 체크' 라는 과정이 수행되었다는 것이다.

TS는 단순히 런타임에 예외를 던지는 코드에 오류를 표시하는 것뿐 아니라, 의도와 다르게 작성된 코드까지 찾으려 한다는 점을 인지하자.
구조적 타이핑 관점에서 한 타입 객체는 정말 넓은 타입을 포함할 수 있기 때문에, 잉여 속성 체크를 이용하면 기본적인 타입 시스템의 구조적 본질을 해치지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않을 수 있다.
(그래서 '엄격한 객체 리터럴 체크' 라고도 불림)
(오직 객체 리터럴에만 적용)

//darkMode 와 darkmode 에 focus
interface Options {
	title: string;
  	darkMode?: boolean;
}

const o: Options = {darkmode: true, title: 'Ski Free'};
//Error : 'Options' 형식에 'darkmode' 가 없습니다.

const intermediate = {darkmode: true. title: 'Ski Free'};
const o: Options = intermediate
// 정상

const o = {darkmode: true. title: 'Ski Free'} as Options;
// 정상
  • 첫 번째 예시는 오른쪽이 객체 리터럴이지만, 두 번째는 객체 리터럴이 아니다.
    따라서 첫 번째는 잉여 속성 체크가 적용되고, 두 번째는 적용되지 않는다.

  • 타입 단언문을 사용할 때도 적용되지 않는다.
    이 예시가 단언문보다 선언문을 사용해야 하는 단적인 예시다. (item 9)


✔️잉여 속성 체크를 원치 않는다면, 인덱스 시그니처를 사용해서 TS가 추가적인 속성을 예상하도록 할 수 있다.

interface Options {
	darkMode?: boolean;
  	[otherOptions: string]: unknown;
}

const o: Options = {darkmode: true}; // 정상

인덱스 시그니처(Index Signature)
{[Key: T]: U} 형식으로 객체가 여러 Key를 가질 수 있으며, Key와 매핑되는 Value를 가지는 경우 사용한다. 객체가 <Key, Value> 형식이며 Key와 Value의 타입을 정확하게 명시해야 하는 경우 사용할 수 있다.

let objSalary {
  bouns: 200,
  pay: 2000,
  allowance: 100,
  incentive: 100
}
// salary의 타입을 인덱스 시그니처(index signature)를 사용해 나타낸 것
function totalSalary(salary: {[key: string]: number}) {
  let total = 0;
  for (const key in salary) {
    total += salary[key];
  }
  return total;
}

✔️선택적 속성만 가지는 '약한(weak) 타입'에는 공통 속성 체크가 동작한다.

interface LineChartOptions {
    logscale ?: boolean;
    invertedYAxis ?: boolean;
    areaChart ?: boolean;
}

const opts = {logScale : true}
const o : LineChartOptions = opts;
// Error . '{logScale: boolean}' 유형에 'LineChartOptions' 유형과 공통적인 속성이 없습니다.

구조적 관점에서 LineChartOptions 는 모든 속성이 선택적이므로 모든 객체를 포함할 수 있지만, 이런 약한 타입에 대해서 TS는 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행한다.

❗공통 속성 체크 또한 잉여 속성 체크와 마찬가지로 오타를 잡는 데 효과적이며 구조적으로 엄격하지 않다. 하지만 '약한 타입' 과 관련된 할당문마다 수행된다. 임시 변수를 제거하더라도 공통 속성 체크는 여전히 동작한다.

☑️잉여 속석 체크는 TS의 타입 체커가 수행하는 일반적인 구조적 할당 가능성 체크와 역할이 다르다.
☑️잉여 속석 체크에는 한계가 있다. 임시 변수를 도입하면 잉여 속성 체크를 건너뛸 수 있다는 점을 기억하자.


item 12. 함수 표현식에 타입 적용하기

JS와 TS에서는 함수 '문장'과 함수 '표현식'을 다르게 인식한다.

function rillDice(sides: numbeR) number {/* ... */} // 함수 문장
const rollDice2 = function(sides: number): number {/*...*/} // 함수 표현식
const rollDice3 = (sides: number) :number => {/* ... */} // 함수 표현식

TS에서는 함수 표현식을 사용하는 것이 좋다.
함수의 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있기 때문이다. 아래 예시와 같이 불필요한 코드의 반복을 줄여준다.

function add(a:number, b:number) {return a+b}
function sub(a:number, b:number) {return a-b}
function mul(a:number, b:number) {return a*b}
function div(a:number, b:number) {return a/b}

type BinaryFn = (a:number, b:number) => number;
const add1: BinaryFn = (a,b) => a+b;
const sub1: BinaryFn = (a,b) => a-b;
const mul1: BinaryFn = (a,b) => a+b;
const div1: BinaryFn = (a,b) => a-b;

라이브러리는 공통 함수 시그니처를 타입으로 제공하기도 한다. 예를 들어 React는 함수의 매개변수에 명시하는 MouseEvent 타입 대신에 함수 전체에 적용할 수 있는 MouseEventHandler 타입을 제공한다.

✔️ 시그니처가 일치하는 다른 함수가 있을 때도 함수 표현식에 타입을 적용해 볼 수 있다.

const responseP = fetch('/quote?by=Mark+Twain') // type > Promise<Response>

async function getQuote() {
    const res = await fetch('/quote?by=Mark+Twain');
    const quote = await res.json();
    return quote;
}

❗위의 getQuote() 함수에는 버그가 있다.
/quote 가 존재하지 않는 API 라면 '404 Not Found'가 포함된 내용을 응답하는데 응답이 JSON 형식이 아닐 수 있다.
이에 따라 res.json() 은 JSON 형식이 아니라는 새로운 오류 메시지를 담아 rejected Promise를 반환한다.
호출한 곳에서는 새로운 오류 메시지가 전달되어 실제 오류인 404가 감추어진다.
또한, fetch가 실패하면 거절된 프로미스를 응답하지는 않으므로 상태 체크를 수행해 줄 checkedFetch 함수를 작성해 보자.

// lib.dom.d.ts 에 정의된 fetch의 타입
declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;

// 이를 이용해 작성한 checkedFetch
async function checkedFetch(input: RequestInfo, init ?:RequestInit) {
    const res = await fetch(input, init);
    if(!res.ok) {
        throw new Error('Request failed:' + res.status);
    }
    return res;
}

// 표현식을 통한 간결화
const checkedFetch: typeof fetch = async(input, init) => {
    const res = await fetch(input, init);
    if(!res.ok) {
        throw new Error('error')
    }
    return res
}

마지막 간결화 코드와 같이 함수 전체에 type 지정을 해주면 TS가 매개변수를 추론할 수 있게 해준다.
함수 '문장'은 '표현식'과 달리 오류가 checkedFetch 구현체가 아닌, 함수를 호출한 위치에서 발생하는 차이점이 있다.

☑️ 함수 표현식 전체에 타입 구문을 적용해보는 것이 좋다.
☑️ 만약 같은 타입 시그니처를 반복적으로 작성한 코드가 있다면 함수 타입을 분리해 내거나 이미 존재하는 타입을 찾아보도록 하자.
☑️ 다른 함수의 시그니처를 참조하려면 typeof fn 을 사용하면 된다.


item13. 타입과 인터페이스의 차이점 알기

TS에서 명명된 타입(named type)을 정의하는 방법은 두 가지가 있다.

type asType = {
	name: string;
  	capital: string;
}

interface asInterface {
	name: string;
  	capital: string;
}

// class 도 쓸 수 있지만, class는 값으로도 쓰일 수 있는 JS 런타임의 개념이다.

대부분의 경우 둘 다 사용해도 되나 그 사이에 존재하는 차이를 분명히 알고, 같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 한다.

✔️ Type과 Interface 의 공통점

➡️ 둘 중 어떤 것으로 정의해도 상태에는 차이가 없다.
➡️ 추가 속성과 함께 할당한다면 동일한 오류가 발생한다.

const www: asType = {
    name: 'www',
    capital: 'dd',
    population: 500,
}

const www:asInterface = {
    name: 'www',
    capital: 'dd',
    population: 500,
}

// Error . ~~ 형식에 할당할 수 없습니다. 개체 리터럴은 알려진 속성만 지정할 수 있으며,
// ~~ 형식에 'population' 이 없습니다.

➡️ 인덱스 시그니처와 함수 타입은 인터페이스와 타입에서 모두 사용할 수 있다.

// Index Signature
type TDict = {[key: string]: string};
interface IDict {
    [key: string] : string;
}

// 함수 타입도 가능하다.
type TFn = (x: number) => string;
interface IFn {
    (x: number): string;
}

const toStrT : TFn = x => '' + x
const toStrI : IFn = x => '' + x

➡️ 모두 제너릭이 가능하다.

type TPair<T> = {
    first: T;
    seconde:T;
}

interface IPair<T> {
    first: T;
    second:T
}

➡️ 타입은 인터페이스를 확장할 수 있으며 반대로도 확장 가능하다.

interface III extends asType {
    population: number;
}

type TTT = asInterface & {population: number;}
  • 여기서 주의할 점은 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지 못한다는 점이다.
  • 복잡한 타입을 확장하고 싶다면 '타입'과 &연산자를 사용해야 한다.

➡️ Class를 구현할 때는 타입과 인터페이스를 모두 사용할 수 있다.

class StateT implements asType {
    name: string = '';
    capital: string = '';
}

class StateI implements asInterface {
    name: string = '';
    capital: string = '';
}

✔️ Type과 Interface 의 차이점

➡️ 유니온 타입은 있지만 유니온 인터페이스라는 개념은 없다.

type AorB = 'a' | 'b'
  • 인터페이스는 타입을 확장할 수 있지만 유니온은 할 수 없는데, 확장이 필요할 때가 있다.
    아래 예시에서 Input 과 Output은 별도의 타입이며 이 둘을 하나의 변수명으로 매핑하는 VariableMap 인터페이스를 만들 수 있다.
type Input = {/* ... */}
type Output = {/* ... */}
interface VariableMap {
	[name: string]: Input | Output;
}
  • 유니온 타입에 name 속성을 붙일 수도 있다.
    하지만, 아래 예시는 인터페이스로 표현할 수 없다.
type NamedVariable = (Input | Output) & {name:string}

➡️ type키워드가 일반적으로 interface보다 쓰임새가 많다.

  • type은 유니온이 될 수도 있고, 매핑된 타입 또는 조건부 타입 같은 고급 기능에 활용되기도 한다.
  • 튜플과 배열 타입도 type 키워드를 통해 더 간결하게 표현할 수 있다.
// type으로 구현
type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];

// interface로는 비슷하게만 구현된다.
interface Tuple {
	0: number;
  	1: number;
  	length: 2;
}
const t: Tuple = [10,20];

❗위처럼 interface로 구현 시 튜플에서 사용할 수 있는 concat 과 같은 메서드를 사용할 수 없다. 그러니까 튜플은 type 키워드로 구현하자.

➡️ interface 에는 type에 없는 '보강(augment)' 이라는 기능이 있다.

interface Istate {
	name: string;
	capital: string;
}

interface Istate {
	population: number;
}

const www : Istate = {
	name: 'ddd',
  	capital: 'ccc',
  	population: 10,
}
// 정상

위 예제처럼 속성을 확장하는 것을 '선언 병합(declaration merging)' 이라고 한다.
❗선언 병합은 타입 선언 파일을 작성할 때 사용되며 이를 지원하기 위해 반드시 interface를 사용하는 표준을 따라야 한다.

  • TS는 여러 버전의 JS 표준 라이브러리에서 여러 타입을 모아 병합한다.

    예를 들어, Array 인터페이스는 lib.es5.d.ts에 정의되어 있고 기본적으로 그곳에 선언된 인터페이스가 사용된다. 그러나 tsconfig.json의 lib 목록에 ES2015를 추가하면 TS는 lib.es2015.d.ts에 선언된 인터페이스를 병합한다. (ex. find 메서드 등) 이들은 병합을 통해 다른 Array 인터페이스에 추가된다. 결과적으로 각 선언이 병합되어 전체 메서드를 가지는 하나의 Array 타입을 얻게 된다.

  • 병합은 선언처럼 일반적인 코드라 언제든지 가능하므로 프로퍼티가 추가되는 것을 원치 않는다면 인터페이스 대신 타입을 사용하자.

✔️ 이제 type과 interface 중 어느 것을 사용해야 할까?

  • 복잡한 타입이라면 고민할 것 없이 Type 별칭을 사용하면 된다.
  • 타입과 인터페이스, 두 가지 방법으로 모두 표현할 수 있는 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 보아야 한다.
    ex) 어떤 API에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는 게 좋다. API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합할 수 있어 유용하기 때문이다.
    but, 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계다. 이땐 타입을 쓰자.

☑️ 한 타입을 type과 interface 두 가지로 작성하는 방법을 터득하자.
☑️ 프로젝트에서 어떤 문법을 사용할지 결정할 때 한 가지 일관된 스타일을 확립하고, 보강 기법이 필요한지 고려하자.

profile
오늘보다 내일 더 성장하는 프론트엔드 개발자가 되기 위해

0개의 댓글