해당 게시물에서는 타입스크립트의 21가지 사용법와 해당 사용법이 의미있는 경우를 알아봅니다.
원문 : https://itnext.io/mastering-typescript-21-best-practices-for-improved-code-quality-2f7615e1fdc3
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 컴파일러는 값을 기반으로 타입을 자동으로 추론합니다.
즉 모든 변수에 수동으로 타입을 지정할 필요가 없습니다.
let name = "John";
타입 추론은 복잡한 타입을 이용해 작업하거나, 함수의 리턴값으로 변수를 초기화할 때 유용합니다.
하지만 시스템 내의 다른 곳에서 타입을 재사용하거나, 문서화가 필요한 경우 명시적으로 타입을 선언하는 것이 좋을 수 있습니다.
3. 린터(Linter)
Linter는 일련의 규칙과 지침을 적용하여 더 나은 코드를 작성하는 데 도움이 되는 도구입니다.
잠재적인 오류를 포착하고 코드의 전반적인 품질을 개선하는 데 도움이 될 수 있습니다.
일관된 코드 스타일을 적용하고 잠재적인 오류를 포착하는 데 도움이 되는
ESLint와 같은 TypeScript에 사용할 수 있는 도구가 있습니다.
세미콜론 누락, 미사용 변수 등 다른 일반적인 문제를 확인하도록 이러한 린터를 설정할 수 있습니다.
4. 인터페이스(Interface)
- 인터페이스는 작업할 데이터의 구조와 속성을 개략적으로 설명하는 객체의 청사진과 같습니다.
- 또한 인터페이스는 특정 타입이 사용되는 모든 위치가 한 번에 업데이트되도록 하여 코드 리팩터링을 더 쉽게 만듭니다.
- 클린 코드를 작성하는데 도움이 되는 친구입니다.
interface User {
name: string;
age: number;
}
let user: User = {name: "John", age: 25};
5. 타입 별칭(Type Aliases)
type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };
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");
}
}
let value: unknown = "hello";
let str: string = value; // Error: Type 'unknown' is not assignable to type 'string'.
9. Object 타입
원문의 해당 항목에 대한 설명이 부족하여 내용을 변경하였습니다.
Object 타입은 프로토타입 체인 상에서 Object 객체의 아래에 있는 모든 값을 의미합니다.
따라서 사실상 Object 객체의 메서드만 타입 지정된 any나 봐도 무관합니다.
모양 검사에는 아무효과가 없습니다.
JSON 직렬화, 역직렬화 시에나 어느 정도 도움이 될 것 같지만, 사용사례는 명확하지 않은 것 같습니다.
10. Never 타입
발생해서는 안되는 값을 의미합니다.
해당 함수, 값을 이런 방식으로 사용할 수 없음을 다른 개발자(컴파일러)에게 알려,
런타임 이전에 쉽게 버그를 감지할 수 있습니다.
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"
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을 사용하는 게 나을 수도 있습니다.)
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 기본 기능으로, 더 나은 타입 안전 코드를 작성하는 데 도움이 되는 미리 정의된 타입 집합을 제공합니다.
커스텀 타입을 이용해 보다 일반적인 타입을 쉽게 만들 수 있도록 해줍니다.
type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;
type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;
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
}
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
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"
'FrontEnd' 카테고리의 다른 글
[CSS] absolute positioning을 grid로 대체하기 (0) | 2023.03.04 |
---|---|
[CSS] 레이아웃 Wrapper 만들기 (0) | 2023.03.03 |
컴포넌트 합성 : 서브 컴포넌트의 모든 것 (0) | 2023.03.01 |
컴포넌트 합성 : 레이아웃 기반 합성 컴포넌트 (0) | 2023.02.28 |
Refactoring React(리팩토링 리액트) : Debugging(디버깅) (2) | 2023.02.26 |