티스토리 뷰

원문 출처: https://medium.com/@maksim_smagin/5-useful-tips-to-make-your-typescript-code-much-better-with-examples-5bf8bfd504c6

 

5 Useful Tips to make Your TypeScript code much better. With Examples.

This 5 really usefull tips will help you to improve your TypeScript code. At least i hope so :) Do not hesitate to leave comments, if you…

medium.com

 

이 글에서는 TypeScript에서의 5가지 팁을 소개합니다. TypeScript 경험이 몇 년 정도 되신 분들도 이 글을 보시면 코드를 개선할 수 있는 방법이 있을 겁니다.

1. any를 사용하지 마세요

any나 any를 반환하는 코드를 가능한 적게 사용하세요. any는 타입 체크를 비활성화합니다. any를 사용하지 않으면 타입이 더욱 신뢰할 수 있고 개발자는 코드가 무엇을 하는지 더욱 확신할 수 있습니다.

실제 예시를 보겠습니다. 저는 백엔드 서비스를 위한 코어 라이브러리를 설계하고 있었습니다. 이를 위해 모든 API에서 사용할 설정 서비스를 만들어야 했습니다. 코어 라이브러리에는 각 서비스에 필요한 필수 변수가 포함된 기본 구성이 있었지만 필요한 경우 서비스는 이를 확장할 수 있습니다.

// 코어 라이브러리의 기본 구성
const baseConfig = {
 name: process.env['npm_package_name'] || '',
 version: process.env['npm_package_version'] || '',
 port: Number(process.env['APP_PORT']),
 host: process.env['APP_HOST'] || '',
 env: process.env['NODE_ENV'] || '',
 logLevel: process.env['LOG_LEVEL'] || ''
};

type BaseConfig = typeof baseConfig;

// 나쁨
export function extendBaseConfig(
 fn: (env: NodeJS.ProcessEnv) => any,
): any {
 const localConfig = fn(process.env);
 return { ...baseConfig, ...localConfig }; // any를 반환
}

interface ConfigA extends BaseConfig {
 redis: {
 host: string;
 port: number;
 password: string;
 }
}

const configA: ConfigA = extendBaseConfig(env => {
 redis: {
 host: env['REDIS_HOST'] || '',
 port: parseInt(env['REDIS_PORT']),
 password: env['REDIS_PASSWORD'] || '',
 },
});

// 좋음
type ParseConfigFn<T> = (env: NodeJS.ProcessEnv) => T;

export function extendBaseConfig<T>(
 fn: ParseConfigFn<T>,
): ReturnType<typeof fn> & BaseConfig {
 const localConfig = fn(process.env);

 const overrideBaseKeys = Object.keys(baseConfig).filter((key) =>
 Object.keys(localConfig).includes(key),
 );

 if (overrideBaseKeys.length) {
 throw new Error(
 'Can not override base configuration keys',
 { cause: overrideBaseKeys }
 );
 }

 return { ...baseConfig, ...localConfig };
}

// 서비스 A, 올바른 유형을 동적으로 가져옴
const configA = extendBaseConfig(env => {
 redis: {
 host: env['REDIS_HOST'] || '',
 port: Number(env['REDIS_PORT']),
 password: env['REDIS_PASSWORD'] || '',
 },
});

첫 번째 경우에는 수동으로 BaseConfig를 확장한 새 인터페이스를 만들었지만, extendBaseConfig 함수는 any를 반환하기 때문에 만든 모든 타입이 쓸모없어집니다. 왜냐하면 base config와 ConfigA 유형 사이에 실제 연결이 없기 때문입니다. 그러나 any를 사용하는 것이 훨씬 쉽습니다. 여기서는 반환 유형에 대한 실제 연결이 없기 때문에 반환 유형에 대해 신경쓰지 않기 때문입니다.

두 번째 경우, extendBaseConfig 함수는 콜백 함수가 반환하는 유형과 base type의 혼합 유형을 반환합니다. 이것은 더욱 깨끗한 구현입니다. 인터페이스를 더 이상 만들 필요가 없으며, 사실 base config와 service config가 실제로 연결되어 있으므로 다른 쪽에 영향을 미치지 않고 변경할 수 없습니다. 그래서 소프트웨어 개발자인 저는 이것을 걱정할 필요가 없습니다 :)

 

2. 유틸리티 타입을 사용하세요

유틸리티 타입은 코드 베이스를 줄이고 기존 타입을 재사용할 수 있으며, 일반적으로 타입 시스템을 더욱 견고하게 만드는 데 도움이되는 매우 강력한 기능입니다.

예를 들어, 이전의 예시에서 User와 UserData를 상속하기보다 id 프로퍼티를 제거하는 데 유틸리티 타입 Omit을 사용할 수 있습니다.

// 나쁨
interface UserData {
 email: string;
 saltedPasswordHash: string;
 createdAt: string;
}

interface User extends UserData {
 id: string;
}

function createUser(userData: UserData) { ... }

// 좋음
interface User {
 id: string;
 email: string;
 saltedPasswordHash: string;
 createdAt: string;
}

function createUser(userData: Omit<User, 'id'>) { ... }

상속 대신 id를 생략할 수 있으므로 더 읽기 쉽고 덜 복잡합니다. Omit 이외에도 많은 유틸리티 타입이 있으며 이를 공식 문서에서 확인할 수 있습니다.

 

3. 제네릭을 사용하세요

제네릭은 코드 재사용에 좋은 방법입니다. 코드가 간결해지고 버그가 줄어들기 때문에 코드의 안정성이 높아집니다. TypeScript에서 제네릭 없이는 구현할 수 없는 것들이 많습니다.

const baseConfig = {
 name: process.env['npm_package_name'] || '',
 version: process.env['npm_package_version'] || '',
 port: Number(process.env['APP_PORT']),
 host: process.env['APP_HOST'] || '',
 env: process.env['NODE_ENV'] || '',
 logLevel: process.env['LOG_LEVEL'] || ''
};

type BaseConfig = typeof baseConfig;

export function extendBaseConfig<T>(
 fn: ParseConfigFn<T>,
): ReturnType<typeof fn> & BaseConfig {
 const localConfig = fn(process.env);
 return { ...baseConfig, ...localConfig };
}

이전 예시와 동일한 코드이지만 여기서는 제네릭에 더 초점을 둡니다.

extendBaseConfig 함수는 콜백 함수가 반환하는 유형과 base type의 혼합 유형을 반환합니다. 이것을 통해 우리는 타입을 동적으로 정의할 수 있습니다. 제네릭 없이는 구현할 수 없으며 이렇게 깨끗하고 도움이 되는 구현을 통해 개발이 훨씬 수월해집니다. 제네릭에 대한 좋은 설명은 공식 문서에서 찾을 수 있습니다.

 

4. 런타임 검증을 사용하세요

TypeScript 프로젝트에서는 런타임 검증을 사용하여 타입으로 해결할 수 없는 버그와 문제를 방지해야 합니다. 또한 이를 사용하면 컨트롤러에서 입력 데이터를 검증하는 용도로만 사용하고 서비스 내부에서는 해당 검증에 의존할 수 있습니다. 이렇게 하면 작성해야 하는 코드 양이 줄어들어 좋습니다. 또한 타입으로 해결할 수 없는 API 호출, 큐, 다른 서비스와의 통합, MongoDB를 사용하는 경우 데이터베이스 검증과 같은 것들을 수행할 수 있습니다.

사용할 수 있는 라이브러리는 정말 많습니다. joi, zod, runtypes, class-validator, 등이 있습니다. 여기서는 타입에만 의존한다면 어떻게 되는지 살펴보겠습니다.

// 인터페이스 선언
interface Person {
 name: string;
 age: number;
}

// 외부 서비스 응답
const data = '{"name": null, "age":25}';

// 나쁨
const person: Person = JSON.parse(data);
console.log(person.name.toLowerCase()); // error, null has no .toLowerCase() fn

// 좋음
function isPerson(obj: unknown): obj is Person {
 return (
 typeof obj['name'] === 'string' &&
 typeof obj['age'] === number &&
 !Number.isNaN(obj['age'])
 );
}

const person = JSON.parse(data);
if (isPerson(person)) {
 console.log(person.name.toLowerCase()); // ok :)
}

여기서는 외부 서비스로부터의 응답을 검증합니다. 첫 번째 경우에는 name이 null이므로 예외가 발생하지만, 두 번째 경우에는 검증했기 때문에 문제가 발생하지 않습니다.

 

5. TypeScript 릴리스를 모니터링하고 의존성을 업데이트하세요

많은 프로그래머들이 TypeScript에서 버그 수정 및 새로운 기능을 모니터링하지 않기 때문에 그들의 작업을 도울 수 있는 많은 새로운 기능을 놓치게 됩니다.

https://github.com/microsoft/TypeScript/releases

TypeScript는 완벽하지 않으며 다른 소프트웨어와 마찬가지로 버그가 있을 수 있습니다. 이러한 버그는 프로젝트에서 실제로 해결할 수 없는 문제를 일으킬 수 있으며 타입 캐스팅 또는 특정 코드의 eslint를 사용하여 문제를 해결하려고 할 수 있습니다. TypeScript 컴파일러가 타입을 제대로 계산하지 못하는 경우 코드를 덜 읽기 쉽고 덜 안정적이며 이 문제를 해결하기 위해 더 많은 코드를 작성해야 합니다

댓글