-
타입스크립트 몰랐던 것 (1) :: 마이구미책 리뷰 2021. 10. 4. 18:26반응형
이 글은 타입스크립트 예제 중심으로 다뤄지고 동작 이해를 위해 정리한 글이다.
타입스크립트가 무엇인지? 왜 사용하는지? 에 대한 글은 아니다.
"이펙티브 타입스크립트" 를 통해 알게되었거나 스스로 조금 더 다듬어놓기 위한 것들이다.
이펙티브 타입스크립트 - http://www.yes24.com/Product/Goods/102124327타입스크립트를 사용한 시점부터 깊게 공부하지 않고 기본적인 개념만 가지고 사용했다.
그 이유는 단순 타입 시스템 목적으로 그때그때 습득하면서 사용해도 크게 문제될 것이 없다고 판단했다.
그 과정중 느꼈던 건, 다른 라이브러리들의 코드 분석이 필요할 때, 생각한대로 타입 선언과 추정이 되지 않을 때 조금 난감했다.
그럴 경우에는 any 를 쓰거나 특정 부분은 타입스크립트를 비활성화 시켜버리곤 했다.
complicatedFn(params: any) { ... } /* eslint-disable-next-line */ /* eslint-disable */
미루고 미루다가 타입스크립트 관련 책을 보게 되었다.
역시나 이 책을 읽으면서 더 혼란을 느끼는 부분이 종종 발생했다.
이것이 의미하는 것은 타입스크립트를 정확히 이해하고 있지 않고 사용했다는 것이 증명되었다.
이번에 새로 알게되거나 조금 더 이해하고 있어야하는 것들을 정리해보았다.
TS Playgroud 와 함께 확인해보면 좋다. (https://www.typescriptlang.org/play)
자세한 내용은 더 많은 내용이 담겨져있는 책을 읽어보길 추천한다.
제네릭 타입에서 매개변수에 extends 를 사용하는 것은 매개변수에 특정 타입을 확장한다고 선언할 수 있다.
function getKey1<K extends string>(val: any, key: K) { return key; } function getKey2(val: any, key: string) { return key } getKey1({}, Math.random() < 0.5 ? 'a': 'b'); // 반환 타입 'a' | 'b' getKey2({}, Math.random() < 0.5 ? 'a': 'b'); // 반환 타입 string
getKey1 의 매개변수 제네릭 타입에 string 을 확장함으로써, 반환 타입은 string 이 아닌 더 직관적으로 'a' | 'b' 로 확인할 수 있다.
typeof 는 타입스크립트, 자바스크립트에 따라 사용 목적이 다를 수 있다.
// javascript typeof if (typeof fn === 'fucntion') {} // typescirpt typeof const person = { name: 'mygumi', gender: 'male' }; type Person = typeof person; const p: Person = { name: '', gender: '' }
타입의 관점에서 타입스크립트의 타입을 반환한다.
값의 관점에서 런타임의 typeof 연산자가 반환한다.
객체 래퍼 타입을 지양하고 기본형 타입을 지향해야한다.
우리는 자바스크립트에서 원시값 형태의 문자열 리터럴인 "string" 과 객체 래퍼 형태의 "String" 은 다르다는 것을 알고 있다.
참고로 string 타입에서 String 에서 제공해주는 메소드들을 사용할 수 있는 이유는 문자열 리터럴(string)에서 메소드 사용 시 객체 타입 String 으로 래핑하여 메소드 호출 후 래핑한 객체 버리는 과정이 진행되기 때문이다.
결론은 타입스크립트에서도 잘 인지해서 타입 선언을 해야한다.
function isGreeting(phrase: String) { return [ 'hello', ].includes(phrase) // TypeError }
객체 리터럴은 잉여 속성 체크를 수행한다.
타입스크립트의 타입 시스템은 덕 타이핑(구조적 타이핑)을 따른다.
덕 타이핑 - https://mygumi.tistory.com/367
interface Room { numDoors: number; ceilingHeightFt: number; } const hotel = { numDoors: 1, ceilingHeightFt: 10, elephant: 'present' } const r: Room = hotel;
Room 인터페이스에 elephant 가 없어도 hotel 객체는 타입 에러 없이 타입 체크를 통과한다.
객체 리터럴 방식의 코드라면 잉여 속성 체크를 통해 elephant 타입 에러가 발생한다.
const hotel: Room = { numDoors: 1, ceilingHeightFt: 10, elephant: 'present' // TypeError }
함수 선언식보다는 함수 표현식에 타입 적용하기
declare function fetch(input: RequestInfo, init?: RequestInit): Promise<Response> // 함수 선언식 => 전달 인자에 타입 설정 async function checkedFetch(input: RequestInfo, init?: RequestInit) { ... } // 함수 표현식 => 전달 인자에 타입 설정없이 타입 추론 가능 const checkedFetch: typeof fetch = async (input, init) => { const response = await fetch(input, init); if (!response.ok) { throw new Error() } return response; }
함수 표현식은 전체 타입에 typeof fetch 를 명시해주면 Input, init 의 타입을 추론할 수 있게 해준다.
위와 같은 상황을 보면 매개변수나 반환 값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용하는 것이 좋아보인다.
type, interface 의 차이
타입스크립트에서는 type 과 interface 키워드를 통해 타입을 정의할 수 있다.
일반적인 경우에서는 별 차이없이 정의되고 동작되기에 두 개의 차이를 이해하지 않고도 사용할 수 있다.
하지만 interface 는 유니온 타입의 확장이 불가능하다.
interface Input { input:number } interface Output { output: number } type NamedVariable = (Input | Output) & { name: string } interface NamedVariable = ... // 표현 불가능 const v: NamedVariable = { input: 1, name: '' };
결국 복잡한 타입, 고급 기능을 위해서는 type 을 사용할 수밖에 없다.
튜플의 경우에도 interface 를 통해 선언하기 위해서는 부자연스러운 모습을 볼 수 있다.
type Pair = [number, number]; interface Tuple { 0: number; 1: number; length: 2 }
하지만 interface 는 유용한 보강 기법이 존재한다.
interface IState { name: string; capital: string; } interface IState { population: number; } const s: IState = { name: 'Korea', capital: 'Seoul', population: 50000000 }
타입을 명시하지 않으면, 타입 체커는 타입을 유추하여 결정해야한다.
타입이 명시되지 않았을 경우, 타입 체커가 타입을 유추하는 과정을 "widening" 이라고 한다.
interface Vector3D {x: number, y: number, z: number;} function getComponent(vector: Vector3D, axis: 'x' | 'y' | 'z') { return vector[axis] } const x1 = 'x'; // 반환 타입 'x' let x2 = 'x'; // 반환 타입 string let x3 = 'x' as const; // 반환 타입 'x' let obj1 = { x: 'x' } // 반환 타입 { x: string } let obj2 = { x: 'x' as const } // 반환 타입 { x: 'x' } const vec = { x: 10, y: 20, z: 30 }; getComponent(vec, x1); getComponent(vec, x2); // TypeError getComponent(vec, x3); getComponent(vec, obj1.x); // TypeError getComponent(vec, obj2.x);
const 활용하면 더 좁은 타입이 된다. (let 변수에도 as const 를 선언해줄 수 있다.)
객체의 경우 각 요소들은 let 으로 할당된 것처럼 다뤄지기 때문에, as const 를 통해 타입을 강제 추론하게 해야한다.
유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기
유니온의 인터페이스의 예를 보자.
interface Layer { layout: FillLayout | LineLayout | PointLayout paint: FillPaint | LinePaint | PointPaint }
위의 문제점은 { FillLayout, FillPaint }, { LineLayout, LinePaint }, { Paintayout, PointPaint } 와 같이 1:1 매칭이 되어야한다.
하지만 위의 인터페이스 상에는 { FillLayout, LinePaint } 와 같은 형태로 매칭될 가능성이 존재한다.
이를 위해 인터페이스의 유니온 + 태그된 유니온(tagged union) 을 활용한 예제이다.
interface FillLayer { type: 'fill'; layout: FillLayout; paint: FillPaint } interface LineLayer { type: 'line'; layout: LineLayout; paint: LinePaint } interface PointLayer { type: 'point'; layout: PointLayout; paint: PointPaint } type Layer = FillLayer | LineLayer | PointLayer function drawLayer(layer: Layer) { if (layer.type === 'fill') { const { layout, paint } = layer; // FillLayer 타입 추론 } else if (layer.type === 'line') { const { layout, paint } = layer; // LineLayer 타입 추론 } else { const { layout, paint } = layer; // PointLayer 타입 추론 } }
any 타입은 가능한 한 좁은 범위에서만 사용하기
any 타입을 지양해야하겠지만 사용해야한다면 다른 곳에 영향이 없어야한다.
interface Foo { foo: any } interface Bar { bar: any } function expressionReturingFoo (): Foo { return { foo: '' } } function processBar (value: Bar) { return {} } function fn() { const x = expressionReturingFoo(); processBar(x); // TypeError return x; }
x 값은 expressionReturingFoo 에 의해 Foo 타입이다.
하지만 processBar 의 전달인자는 Bar 로 전달해야하기 때문에 타입 에러가 발생한다.
이것을 해결하기 위해 x 의 타입을 any 로 선언해보자.
function fn() { const x: any = expressionReturingFoo(); processBar(x); return x; } fn(); // 반환 타입 any
타입 에러는 피했지만, fn() 반환 타입이 any 로 설정되었다.
x 에 대한 타입은 processBar() 를 호출할 시점에만 변환하고 다른 곳에서는 영향이 가지 않도록 설정할 수 있다.
function fn() { const x = expressionReturingFoo(); processBar(x as any); return x; } fn(); // 반환 타입 Foo
any 타입의 진화 이해하기
noImplicitAny = true 설정 시에는 암시적 any 상태인 변수에 어떠한 할당도 하지 않고 사용하려고 하면 암시적 any 오류 발생한다.
function range(start: number, limit: number) { const out = []; // TypeError return out; // TypeError }
암시적 any 타입에 어떠한 값을 할당할 때 any 타입의 진화가 발생한다.
function range(start: number, limit: number) { const out = []; for (let i = start; i < limit; i++) { out.push(i); } return out; // 타입 추론 number[] }
out 변수는 암시적으로 any[] 타입을 가지고 있지만, 루프를 통해 number[] 타입으로 진화하게 되었다.
다만, 아래 예제처럼 함수 호출을 거치는 것은 진화가 일어나지 않는다.
function range(start: number, limit: number) { const out = []; // TypeError [1,2,3].forEach((v => { out.push(v) })); return out; // TypeError }
any 대신 unknown 사용하기
any 타입의 특징은 다음과 같다.
- 어떠한 타입이든 any 타입에 할당 가능하다.
- any 타입은 어떠한 타입으로도 할당 가능하다.
unknown 타입의 경우 1번은 만족하지만, 2번은 만족하지 못한다.
let unknownValue: unknown = 123; let anyValue: any = 123; let newValue = true; anyValue = ''; unknownValue = ''; newValue = anyValue; newValue = unknownValue; // TypeError
제공되지 않는 타입 export 하기
외부 라이브러리를 사용할 경우에 대부분의 타입을 제공해주겠지만 그 중 특정 타입을 제공해주지 않을 수 있다.
본인은 그냥 무시하거나 직접 만들어서 사용하곤 했다.
단순하게 생각하니 그 타입을 직접 다른 뽑는 것도 방법일 수 있다.
interface SecretName {} interface SecretSanta {} export function getGift(name: SecretName, gift: string): SecretSanta {}
위의 예제처럼 SecretName, SecretSanta 인터페이스가 export 로 선언되어있지않아 외부 라이브러리의 타입을 사용할 수 없다.
이런 경우 export 할 수 있는 getGift 를 활용해서 뽑아낼 수 있다.
type MySanta = ReturnType<typeof getGift>; type MyName = Parameters<typeof getGift>[0];
타입도 조건부 연산자를 활용할 수 있다.
function double(x: string | number): number | string; function double(x: any) { return x + x; } double('1'); // 반환 타입 number | string
위 예제의 경우 반환 값은 '11' 로써, 우리가 원하는 반환 타입은 string 이다.
하지만 number | string 으로 추론된다.
이를 대응하기 위해 타입에도 조건부 연산자를 활용할 수 있다.
function double<T extends number|string>(x: T): T extends string ? string : number; function double(x: any) { return x + x }; double('1'); // 반환 타입 string double(1); // 반환 타입 number
enum 보다는 문자열을 유니온 타입으로 사용하기
타입스크립트에서 enum 의 경우 import 하고 문자열 대신 사용해야한다.
enum Flavor { Vanilla = 'vanilla', Choclate = 'choclate', Strawberry = 'strawberry' } function scoop(v: Flavor) {} scoop('choclate'); // TypeError scoop(Flavor.Choclate);
위 코드는 구조적 타이핑이 아닌 명목적 타이핑이 되어버린다.
유니온 타입으로 선언하면 타입 추론과 자동 완성이 동일하게 지원되기에 아래의 코드가 더 자연스러워보인다.
// 유니온 타입 1 type Flavor = 'vanilla' | 'choclate' | 'strawberry' function scoop(v: Flavor) {} scoop('choclate'); // 유니온 타입 2 const FlavorType = { Vanilla: 'vanilla', Choclate: 'choclate', Strawberry: 'strawberry' } as const type Values<T> = T[keyof T]; type Flavor = Values<typeof FlavorType> function scoop(v: Flavor) {} scoop("choclate")
private 를 위한 기능을 3.8 버전부터 private field 를 제공해준다
아시다시피 타입스크립트는 런타임 시점이 아닌 컴파일 시점이다.
타입스크리븥에서 private 키워드를 제공해주더라도 런타임 시점에는 없어지기 때문에 원래의 목적인 private 목적을 이룰 수 없다.
3.8 버전부터 private field 라는 용어로 제공해주고 있다.
class Animal { #name: string; constructor(theName: string) { this.#name = theName; } } new Animal("Cat").#name; // #name 접근 불가 에러
반응형'책 리뷰' 카테고리의 다른 글
이펙티브 엔지니어 리뷰 :: 마이구미 (0) 2022.10.19 UX 라이팅 시작하기 리뷰 :: 마이구미 (0) 2022.06.09 가상 면접 사례로 배우는 대규모 시스템 설계 기초 리뷰 :: 마이구미 (4) 2021.09.16 개발 7년차, 매니저 1일차 리뷰 :: 마이구미 (0) 2021.08.14 팀장은 처음이라 리뷰 :: 마이구미 (0) 2021.07.11