엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드의 타입과 추상화 수준을 높여준다.
잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어준다.
동물들의 데이터베이스를 구축한다고 가정해보자. 인터페이스는 다음과 같다.
interface Animal {
name: string;
endangered: boolean;
habitat: string;
}
const leopard: Animal = {
name: 'Snow Leopard',
endangered: false,
habitat: 'tundra',
}
=> 의도가 뭔지 모르겠다.... 타입스크립트는 의도를 명시하기에 아주 좋은 친구인데 말이지... 잘써먹지 못하고 있다.
이 문제들을 해결하려면 해당 속성을 작성한 사람을 찾아서 물어봐야한다. 하지만 그사람은 이미 없...다..
interface Animal {
commonName: string;
genus: string;
species: string;
status: ConservationSstatus;
climates: KoppenClimate[];
}
type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | 'BSk' | 'BWh' | 'BWk' |
'Cfa' | 'Cfb' | 'Cfc' |'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' | 'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd' | 'EF' | 'ET';
const snowLeopard: Animal = {
commonName: 'Snow Leopard',
genus: 'Panthera',
species: 'Uncia',
status: 'VU', //취약종(vulnerable)
climates: ['ET', 'EF', 'Dfd'], //고산대(alpine) 또는 아고산대(subalpine)
}
이 코드는 다음 세 가지를 개선했다.
데이터를 명확하게 표현하고 있고, 정보를 찾기위해 사람에 의존할 필요없다. 온라인에 무수히 많은 정보가 있다.
코드로 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있따. 자체적으로 용어를 만들어 내려고 하지 말고, 해당 분야에 이미 존재하는 용어를 사용해야한다.
구조적 타이핑 특성 때문에 가끔 코드가 이상한 결과를 낼 수 있다.
interface Vector2D {
x: number;
y: number;
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
calculateNorm({x: 3, y: 4}); // 정상, 결과는 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D); //정상! 결과는 동일하게 5
interface Vector2D {
_brand: '2d';
x: number;
y: number;
}
function vec2D(x: number, y: number): Vector2D {
return {x, y, _brand: '2d'}
}
function calculateNorm(p: Vector2D){
return Math.sqrt(p.x * p.x + p.y * p.y); //기존과 동일하다.
}
calculateNorm(vec2D(3,4)); // 정상, 결과는 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);
// '_brand' 속성 ... 형식에 없습니다.
예시) 절대 경로를 사용해 파일 시스템에 접근하는 함수를 가정해 보자.
런타임에는 절대 경로('/')로 시작하는지 체크하기 쉽지만, 타입시스템에서는 절대 경로를 판단하기 어렵기 때문에 상표 기법을 사용한다.
type AbsolutePath = string & {_brand: 'abs'};
function listAbsolutePath(path: AbsolutePath) {
// ...
}
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith('/');
}
string 타입이면서 _brand 속성을 가지는 객체를 만들 수는 없다. AbsolutePath는 온전히 타입 시스템의 영역이다.
만약 path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면, 타입을 정제해 주는 타입 가드를 사용해서 오류를 방지할 수 있다.
function f(path: string) {
if (isAbsolutePath(path)) {
listAbsolutePath(path);
}
listAbsolutePath(path);
// 'string' 형식의 인수는 'AbsolutePath' 형식에 매개 변수에 할당될 수 없다.
}
로직을 분기하는 대신에 오류가 발생한 곳에 path as AbsolutePath를 사용해서 오류를 제거할 수도 있지만 단언문은 지양해야한다.
단언문을 쓰지 않고 AbsolutePath 타입을 얻기 위한 유일한 방법은 AbsolutePath 타입을 매개변수로 받거나 타입이 맞는지 체크하는것 뿐이다.
function processBar(b:Bar) { /* ... */ }
function f() {
const x = expressionReturningFoo();
processBar(x);
// ~'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
문맥 상으로 x라는 변수가 동시에 Foo 타입과 Bar 타입에 할당 가능하다면, 오류를 제거하는 방법은 두가지다.
function f1() {
const x: any = expressionReturningFoo(); //❌
processBar(x)
}
function f2() {
const x = expressionReturningFoo();
processBar(x as any); //차라리 이게 낫다.
}
f1 보다 f2가 나은 이유는 any 타입이 processBar 함수의 매개변수에서만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않기 때문이다.
f1에서는 함수의 마지막까지 x의 타입이 any인 반면, f2에서는 processBar 호출 이후에 x가 그대로 Foo 타입이다.
만일! f1 함수가 x를 반환한다면 문제가 커진다..
function f1() {
const x: any = expressionReturningFoo();
processBar(x)
return x;
}
function g() {
const foo = f1() //타입이 any
foo.fooMethod(); // 이 함수 호출은 체크 되지 않는다.
}
g 함수 내에서 f1이 사용되므로 f1의 반환 타입인 any 타입이 foo의 타입에 영향을 미친다. 이렇게 함수에서 any를 반환하면 그 영향력은 프로젝트 전반에 퍼진다..
반면 f2를 사용하면 any 타입이 함수 바깥으로 영향을 미치지 않는다.
⭐️ 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다. 함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지 할 수 있다.
객체도 살펴보자
const config: Config = {
a: 1,
b: 2,
c: {
key: value
// 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없다.
}
}
얘보단
const config: Config = {
a: 1,
b: 2,
c: {
key: value
}
} as any; //이렇게 하지말자
얘가 낫다.
const config: Config = {
a: 1,
b: 2, //이 속성은 여전히 체크된다.
c: {
key: value as any;
}
}
any는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입이다.
any 타입에는 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘리먼트는 물론 null과 undefined 까지 포함된다.
any보다 구체적인 타입을 찾아 타입 안전성을 높여야한다.
function getLengthBad(array: any) { // 이렇게 하지 말자.
return array.length;
}
function getLength(array: any[]){
return array.length;
}
any를 사용하는 getLengthBad 보다는 any[]를 사용하는 getLength가 더 좋은 함수이다.
배열이 아닌 값을 넣어서 실행해 보면, getLength는 제대로 오류를 표시하지만 getLengthBad는 오류를 잡아내지 못한다.
함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[key: string]: any} 처럼 선언하면 된다.
function hasTwelveLetterkey(o: {[key: string]: any}){
for(const key in o){
if(key.length === 12){
return true;
}
}
return false
}
함수의 매개변수가 객체지만 값을 알수 없다면 {[key: string]: any} 대신 모든 비기본형(non-primitive) 타입을 포함하는 object 타입을 사용할 수도 있다. object 타입은 객체의 키를 열거할 수는 있지만 속성에 접근할 수 없다는 점에서 {[key: string]: any} 와 약간다르다.
function hasTwelveLetterkey(o: object){
for(const key in o){
if(key.length === 12){
console.log(key, o[key]);
// '{}' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
return true;
}
}
return false
}
함수의 타입에도 단순히 any를 사용해서는 안된다. 최소한 구체화할 수 있는 방법이 세가지 있다.
type Fn0 = () => any; //매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; //매개변수 1개
type FnN = (...args: any[]) => any; //모든 개수의 매개변수
//"Function" 타입과 동일하다.
모두 any보다는 구체적이다. 마지막 줄을 잘 살펴보면 ...args 의 타입을 any[]로 선언했다. any로 선언해도 동작하지만 any[]f로 선언했다. any로 선언해도 동작하지만 any[]로 선언하면 배열 형태라는 것을 알 수 있어 더 구체적이다.
const numArgsBad = (...args: any) => args.length; //any를 반환한다.
const numArgsGood = (...args: any[]) => args.length //number를 반환한다.
함수를 작성하다 보면, 외부로 드러난 타입 정의는 간단하지만 내부 로직이 복잡해서 안전한 타입으로 구현하기 어려운 경우가 만다.
함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 게 낫다.
제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계이다.
예) 어떤 함수가 자신의 마지막 호출을 캐시하도록 만든다고 가정해보자.
어떤 함수든 캐싱할 수 있도록 래퍼 함수 cacheLast를 만들어보자.
declare function cacheLast<T extends Function>(fn: T): T;
declare function shaalowEqual(a: any, b:any): boolean;
function cacheLast<T extends Function>(fn: T): T {
let lastArgs: any[]|null = null;
let lastResult: any;
return function(...args: any[]){
//'(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없다.
if(!lastArgs || !shallowEqual(lastArgs, args)){
lastResult = fn(...args);
lastArgs = args;
}
return lastResult;
}
}
타입스크립트는 반환문에 있는 함수와 원본 함수 T타입이 어떤 관련이 있는지 알지 못하기 때문에 오류가 발생했다. 그러나 결과적으로 원본 함수 T타입과 동일한 매개변수로 호출되고 반환값 역시 예상한 결과가 되기 떄문에 타입 선언문을 추가해서 오류를 제거하는 것이 문제가 되지 않는다.
declare function cacheLast<T extends Function>(fn: T): T;
declare function shaalowEqual(a: any, b:any): boolean;
function cacheLast<T extends Function>(fn: T): T {
let lastArgs: any[]|null = null;
let lastResult: any;
return function(...args: any[]){
//'(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없다.
if(!lastArgs || !shallowEqual(lastArgs, args)){
lastResult = fn(...args);
lastArgs = args;
}
return lastResult;
} as unknown as T;
}
함수 내부에는 any가 꽤 많이 보이지만 타입 정의에는 any가 없기 때문에, cacheLast를 호출하는 쪽에서는 any가 사용됐는지 모름