본문 바로가기

FrontEnd

[Typescript] 타입스크립트의 21가지 사용법

반응형

typescript logo

해당 게시물에서는 타입스크립트의 21가지 사용법와 해당 사용법이 의미있는 경우를 알아봅니다.

원문 : https://itnext.io/mastering-typescript-21-best-practices-for-improved-code-quality-2f7615e1fdc3

 

🔥 Mastering TypeScript: 21 Best Practices for Improved Code Quality

Achieve Typescript mastery with a 21-steps guide, that takes you from Padawan to Obi-Wan.

itnext.io


1. 정적 타입 검사(Strict Type Checking)

정적 타입 검사의 목표는 런타임(코드 실행) 이전에 오류를 조기 발견하는 것입니다.

실행 전에도 테스트 실패를 알 수 있는 테스트 코드와 같죠.

만약 이런 한 줄의 코드가 거대한 프로젝트 안에 숨어 있다 가정하면,

해당 코드의 오류를 발견하는데는 수시간이 걸릴 수도 있습니다.

타입스크립트를 이용하면 이를 캐치해 내는 것이 매우 쉽습니다.

let userName: string = "John";
userName = 123; // TypeScript will raise an error because "123" is not a string.

2. 타입 추론(Type Inference)

TypeScript의 각 값은 명시적인 타입을 갖고 있지만, 모든 것을 타이핑할 필요는 없습니다.
Typescript 컴파일러는 값을 기반으로 타입을 자동으로 추론합니다.
즉 모든 변수에 수동으로 타입을 지정할 필요가 없습니다.

 

예를 들어 다음 코드 스니펫에서 TypeScript는 name 의 타입을 string으로 자동으로 유추합니다.
let name = "John";

타입 추론은 복잡한 타입을 이용해 작업하거나, 함수의 리턴값으로 변수를 초기화할 때 유용합니다.
하지만 시스템 내의 다른 곳에서 타입을 재사용하거나, 문서화가 필요한 경우 명시적으로 타입을 선언하는 것이 좋을 수 있습니다.


3. 린터(Linter)

Linter는 일련의 규칙과 지침을 적용하여 더 나은 코드를 작성하는 데 도움이 되는 도구입니다.
잠재적인 오류를 포착하고 코드의 전반적인 품질을 개선하는 데 도움이 될 수 있습니다.

일관된 코드 스타일을 적용하고 잠재적인 오류를 포착하는 데 도움이 되는
ESLint와 같은 TypeScript에 사용할 수 있는 도구가 있습니다.
세미콜론 누락, 미사용 변수 등 다른 일반적인 문제를 확인하도록 이러한 린터를 설정할 수 있습니다.


4. 인터페이스(Interface)

  • 인터페이스는 작업할 데이터의 구조와 속성을 개략적으로 설명하는 객체의 청사진과 같습니다.
  • 또한 인터페이스는 특정 타입이 사용되는 모든 위치가 한 번에 업데이트되도록 하여 코드 리팩터링을 더 쉽게 만듭니다.
    • 클린 코드를 작성하는데 도움이 되는 친구입니다.
TypeScript의 인터페이스는 객체의 모양에 대한 계약을 정의합니다.
해당 타입의 객체가 가져야 하는 속성과 메서드를 지정하고 변수의 타입으로 사용할 수 있습니다.
즉, 인터페이스 타입이 지정된 변수에 객체를 할당할 때
TypeScript는 객체에 인터페이스에 지정된 모든 속성과 메서드가 있는지 확인합니다.
interface User {
 name: string;
 age: number;
}
let user: User = {name: "John", age: 25};


5. 타입 별칭(Type Aliases)

TypeScript의 타입 별칭 기능을 사용하여 사용자 지정 타입을 만들 수 있습니다
타입  별칭과 인터페이스의 주요 차이점은,
인터페이스는 객체 모양의 새 이름을 생성하는 반면,
타입 별칭은 타입의 새 이름을 생성한다는 것입니다.
 
타입 별칭을 사용하여 2차원 공간의 점에 대해 사용자 지정 타입을 만들 수 있습니다.
type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };
타입 별칭은 union 타입 또는 intersection 타입과 같은 복합 타입(complex type)을 작성하는 데에도 사용할 수 있습니다.
type User = { name: string, age: number };
type Admin = { name: string, age: number, privileges: string[] };
type SuperUser = User & Admin;

6. 튜플(Tuples)

튜플 타입은 다른 타입의 요소들을 포함한 고정 크기 배열을 나타냅니다.
이를 통해 특정 순서 및 타입 값 모음을 표현할 수 있습니다.
예를 들어 튜플을 사용하여 2D 공간의 점을 나타낼 수 있습니다.

let point: [number, number] = [1, 2];
튜플을 사용하여 여러 타입의 컬렉션을 나타낼 수도 있습니다.
let user: [string, number, boolean] = ["Bob", 25, true];

튜플 사용의 주요 이점 중 하나는 컬렉션 요소 간 특정 타입 관계를 표현하는 방법을 제공한다는 것입니다.
또한 구조 분해 할당을 사용하여 튜플의 요소를 추출하여 변수에 할당할 수 있습니다.

 
let point: [number, number] = [1, 2];
let [x, y] = point;
console.log(x, y);

7. any 타입(any)

때떄로 변수 타입에 대해 모든 정보가 없는 경우가 있습니다.

이 경우는 어쩔수 없이 any를 사용해야 합니다.

주로 동적으로 생성된 데이터나 타입 지원이 없는 서드 파티 라이브러리를 사용할 때입니다.

대신 any로 사용돠는 변수의 값을 최대한 좁힐 수 있도록 앞뒤로 타입 가드 혹은 타입 단언을 추가하는 것이 좋습니다.

function logData(data: any) {
    console.log(data);
}

const user = { name: "John", age: 30 };
const numbers = [1, 2, 3];

logData(user); // { name: "John", age: 30 }
logData(numbers); // [1, 2, 3]

할 수 있다면 일반적인 타입에는 object 혹은 unknown을 사용하고 any를 피하는 것이 좋습니다.


8. unknown 타입

any 타입과 달리 unknown 타입을 사용하면,
TypeScript는 타입을 먼저 확인하지 않는 한 값을 이용한 연산을 수행하는 것을 허용하지 않습니다.
이렇게 하면 런타임이 아닌 컴파일 타임에 타입 오류를 포착하는 데 도움이 될 수 있습니다.

 

아래와 같이 타입 안전한 동적 코드를 만들 수 있습니다.

function printValue(value: unknown) {
 if (typeof value === "string") {
 console.log(value);
 } else {
 console.log("Not a string");
 }
}
unknown 타입을 사용하여 더 타입 안전한 변수를 만들 수도 있습니다.
let value: unknown = "hello";
let str: string = value; // Error: Type 'unknown' is not assignable to type 'string'.

9. Object 타입

원문의 해당 항목에 대한 설명이 부족하여 내용을 변경하였습니다.

Object 타입은 프로토타입 체인 상에서 Object 객체의 아래에 있는 모든 값을 의미합니다.
따라서 사실상 Object 객체의 메서드만 타입 지정된 any나 봐도 무관합니다.

모양 검사에는 아무효과가 없습니다.

Oject

JSON 직렬화, 역직렬화 시에나 어느 정도 도움이 될 것 같지만, 사용사례는 명확하지 않은 것 같습니다.


10. Never 타입

발생해서는 안되는 값을 의미합니다.
해당 함수, 값을 이런 방식으로 사용할 수 없음을 다른 개발자(컴파일러)에게 알려, 

런타임 이전에 쉽게 버그를 감지할 수 있습니다.

 

예를 들어 입력이 0보다 작은 경우 오류를 발생시키는 다음 함수가 있으면,
function divide(numerator: number, denominator: number): number {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

이 함수가 정상적으로 반환되지 않음을 나타내기 위해 리턴 타입으로 never를 사용할 수 있습니다.

function divide(numerator: number, denominator: number): number | never {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

11. keyof 연산자

keyof 연산자는 객체의 키를 나타내는 타입을 생성하는 데 사용합니다.
객체 허용 속성을 명확하게 할 수 있습니다.

 

아래와 같이 키 값을 따로 추출하여 타입의 가독성을 높일 수 있습니다.

interface User {
 name: string;
 age: number;
}
type UserKeys = keyof User; // "name" | "age"
keyof 연산자를 사용하여 객체와 객체 키를 인수로 사용하는 보다 타입 안전한 함수를 만들 수 있습니다.
function getValue<T, K extends keyof T>(obj: T, key: K) {
 return obj[key];
}
let user: User = { name: "John", age: 30 };
console.log(getValue(user, "name")); // "John"
console.log(getValue(user, "gender")); // Error: Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'.

12. Enum(열거형)

Enumeration의 축약어인 enum은 명명된 상수 집합을 정의하는 방법입니다.
관련 값 집합에 의미 있는 이름을 지정하여 더 읽기 쉽고 유지 관리하기 쉬운 코드를 만드는 데 사용할 수 있습니다.

(주, 트리 셰이킹과 타입과 값이 혼재되어 있는 애매한 속성 때문에 union을 사용하는 게 나을 수도 있습니다.)

 

예를 들어 열거형을 사용하여 order에 대해 가능한 상태 값 집합을 정의할 수 있습니다.
enum OrderStatus {
 Pending,
 Processing,
 Shipped,
 Delivered,
 Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;
열거형에는 사용자 지정 숫자 값 또는 문자열 집합을 지정할 수 있습니다.
enum OrderStatus {
 Pending = 1,
 Processing = 2,
 Shipped = 3,
 Delivered = 4,
 Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;

13. Namespaces

네임스페이스는 코드를 정돈하고 이름 충돌을 방지하는 방법입니다.
변수, 클래스, 함수 및 인터페이스를 정의할 수 있는 코드 컨테이너를 만들 수 있습니다.

namespace OrderModule {
 export class Order { /* … */ }
 export function cancelOrder(order: Order) { /* … */ }
 export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);
네임스페이스를 사용하여 코드에 고유한 이름을 제공하여 이름 충돌을 방지할 수도 있습니다.
namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();

네임스페이스는 모듈과 유사하지만 코드를 정돈하고 이름 충돌을 방지하는 데 사용되는 반면,
모듈은 코드를 로드하고 실행하는 데 사용됩니다.

(주 : 그냥 객체로 선언하고 파일을 분리해 모듈로 만드는게 여러모로 더 나을 가능성이 있습니다.)

14. Utility Types

유틸리티 타입은 TypeScript 기본 기능으로, 더 나은 타입 안전 코드를 작성하는 데 도움이 되는 미리 정의된 타입 집합을 제공합니다.
커스텀 타입을 이용해 보다 일반적인 타입을 쉽게 만들 수 있도록 해줍니다.

 

 

예를 들어 Pick 유틸리티 타입을 사용하여 객체 타입에서 속성의 하위 집합을 추출할 수 있습니다.
type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;
Exclude 유틸리티 타입을 사용하여 객체 타입에서 속성을 제거할 수도 있습니다.
type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;
Partial 유틸리티 유형을 사용하여 타입의 모든 속성을 선택적으로 만들 수 있습니다.
type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

15. Readonly와 Readonly Array

Readonly 및 ReadonlyArray를 이용해 데이터로 작업할 때 특정 값을 변경할 수 없도록 할 수 있습니다.

Readonly 키워드는 객체 속성을 읽기 전용으로 만드는 데 사용됩니다. 
즉, 객체를 만든 후에는 수정할 수 없음을 의미합니다.
이는 예를 들어 설정 또는 상수 값으로 작업할 때 유용할 수 있습니다.

interface Point {
 x: number;
 y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript will raise an error because "point.x" is read-only

ReadonlyArray는 Readonly와 비슷하지만 배열에 사용됩니다.
배열을 읽기 전용으로 만들어 생성 후에는 수정할 수 없도록 합니다.

let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript will raise an error because "numbers" is read-only

16. Type Guards

복잡한 타입으로 작업할 때 변수의 다양한 가능성을 추적하는 것이 어려울 수 있습니다.
타입 가드는 특정 조건에 따라 변수 타입의 범위를 좁히는 데 도움이 되는 강력한 도구입니다.

다음은 변수가 숫자인지 확인하기 위해 타입 가드를 사용하는 방법의 예입니다.
function isNumber(x: any): x is number {
 return typeof x === "number";
}
let value = 3;
if (isNumber(value)) {
 value.toFixed(2); // TypeScript knows that "value" is a number because of the type guard
}
타입 가드는 "in" 연산자, typeof 연산자 및 instanceof 연산자와 함께 사용할 수 있습니다.

17. Generics

제네릭을 사용하면 각 타입에 대해 별도의 구현을 작성할 필요 없이
여러 타입에서 작동할 수 있는 단일 함수, 클래스 또는 인터페이스를 작성할 수 있습니다.
예를 들어 일반 함수를 사용하여 모든 유형의 배열을 만들 수 있습니다.
function createArray<T>(length: number, value: T): Array<T> {
 let result = [];
 for (let i = 0; i < length; i++) {
 result[i] = value;
 }
 return result;
}
let names = createArray<string>(3, "Bob");
let numbers = createArray<number>(3, 0);
제네릭을 사용하여 모든 타입의 데이터와 함께 작동하는 클래스를 만들 수도 있습니다.
class GenericNumber<T> {
 zeroValue: T;
 add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

더 읽어보기 : writing-constructor-typescript


 

 

18. infer

infer 키워드는 타입에서 타입을 추출할 수 있도록 해줍니다.
예를 들어 infer 키워드를 사용하여 특정 타입의 배열을 반환하는 함수에 대해 보다 정확한 타입을 만들 수 있습니다.

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray is of type string
또한 infer 키워드를 사용하여 특정 속성을 가진 객체를 반환하는 함수에 대한 보다 정확한 타입을 만들 수 있습니다.
type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject is of type {name:string, age: number}

19. Conditional Types (조건부 타입)

조건부 타입을 사용하여 타입의 조건에 따라 새 타입을 만들 수 있습니다.

즉, 더 복잡한 타입 간 관계를 표현할 수 있습니다.

예를 들어 조건부 타입을 사용하여 함수의 반환 타입을 추출할 수 있습니다.
type ReturnType<T> = T extends (…args: any[]) => infer R ? R : any;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => void>; // void
조건부 타입을 사용하여 특정 조건을 충족하는 객체 타입의 속성을 추출할 수도 있습니다.
type PickProperties<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
type P1 = PickProperties<{ a: number, b: string, c: boolean }, string | number>; // "a" | "b"

20. Mapped Types (매핑된 타입)

매핑된 타입은 기존 타입을 이용해 새로운 타입을 만드는 방법압니다.

예를 들어 기존 타입을 이용해 읽기 전용 버전의 새로운 타입을 만들 수 있습니다.

type Readonly<T> = { readonly [P in keyof T]: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let readonlyObj: Readonly<typeof obj> = { a: 1, b: "hello" };
매핑된 타입을 사용해 선택적 버전을 만들 수도 있습니다.
type Optional<T> = { [P in keyof T]?: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let optionalObj: Optional<typeof obj> = { a: 1 };

21. Decorator

데코레이터는 간단한 구문을 사용하여 클래스의 메서드 또는 속성에 추가 기능을 추가하는 방법입니다.
클래스의 구현을 수정하지 않고 클래스의 동작을 향상시키는 방법입니다.

예를 들어 데코레이터를 사용하여 메서드에 로깅을 추가할 수 있습니다.
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 let originalMethod = descriptor.value;
 descriptor.value = function(…args: any[]) {
 console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
 let result = originalMethod.apply(this, args);
 console.log(`Called ${propertyKey}, result: ${result}`);
 return result;
 }
}
class Calculator {
 @logMethod
 add(x: number, y: number): number {
 return x + y;
 }
}
데코레이터를 사용하여 런타임에 사용할 수 있는 클래스, 메서드 또는 속성에 메타데이터를 추가할 수도 있습니다.
function setApiPath(path: string) {
 return function (target: any) {
 target.prototype.apiPath = path;
 }
}
@setApiPath("/users")
class UserService {
 // …
}
console.log(new UserService().apiPath); // "/users"
반응형