| name | typescript-refactor |
| description | TypeScript 코드를 리팩토링하고 타입 안정성을 개선합니다. 타입 에러 수정, 제네릭 추가, 인터페이스 재구성, 유틸리티 타입 활용, 또는 strict mode 마이그레이션이 필요할 때 사용하세요. |
TypeScript Refactoring Skill
TypeScript 5.8.3 strict mode 환경에서 타입 안정성과 코드 품질을 향상시킵니다.
When to Use This Skill
- any 타입을 제거하고 구체적인 타입으로 변경할 때
- 타입 에러를 수정할 때 (null/undefined, 타입 불일치 등)
- 제네릭을 추가하여 재사용성을 높일 때
- Union Type을 Discriminated Union으로 리팩토링할 때
- Utility Types(Partial, Pick, Omit 등)를 활용할 때
- React Props, Event Handler, Ref 타입을 정의할 때
- API 응답 타입 정의 및 타입 가드를 추가할 때
프로젝트 TypeScript 설정
tsconfig.app.json
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
일반적인 리팩토링 패턴
1. Any 타입 제거
Before:
function processData(data: any) {
return data.map((item: any) => item.name);
}
After:
interface DataItem {
id: string;
name: string;
}
function processData(data: DataItem[]): string[] {
return data.map((item) => item.name);
}
2. 타입 가드 활용
Before:
function handleResponse(response: unknown) {
if (response) {
console.log(response.data);
}
}
After:
interface ApiResponse {
data: unknown;
status: number;
}
function isApiResponse(value: unknown): value is ApiResponse {
return (
typeof value === 'object' &&
value !== null &&
'data' in value &&
'status' in value
);
}
function handleResponse(response: unknown) {
if (isApiResponse(response)) {
console.log(response.data);
}
}
3. 제네릭 활용
Before:
function getFirstItem(items: any[]): any {
return items[0];
}
After:
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = getFirstItem([1, 2, 3]);
const firstName = getFirstItem(['a', 'b']);
4. Union Type을 Discriminated Union으로
Before:
interface Resource {
id: string;
type: 'model' | 'service' | 'workflow';
modelConfig?: ModelConfig;
serviceConfig?: ServiceConfig;
workflowConfig?: WorkflowConfig;
}
After:
interface BaseResource {
id: string;
}
interface ModelResource extends BaseResource {
type: 'model';
modelConfig: ModelConfig;
}
interface ServiceResource extends BaseResource {
type: 'service';
serviceConfig: ServiceConfig;
}
interface WorkflowResource extends BaseResource {
type: 'workflow';
workflowConfig: WorkflowConfig;
}
type Resource = ModelResource | ServiceResource | WorkflowResource;
function handleResource(resource: Resource) {
switch (resource.type) {
case 'model':
console.log(resource.modelConfig);
break;
case 'service':
console.(resource.);
;
:
.(resource.);
;
}
}
5. Optional Chaining & Nullish Coalescing
Before:
const userName = user && user.profile && user.profile.name
? user.profile.name
: 'Anonymous';
After:
const userName = user?.profile?.name ?? 'Anonymous';
6. Non-null Assertion 제거
Before:
function getUser(id: string) {
const user = users.find(u => u.id === id);
return user!.name;
}
After:
function getUser(id: string): string | null {
const user = users.find(u => u.id === id);
return user?.name ?? null;
}
function getUserOrThrow(id: string): string {
const user = users.find(u => u.id === id);
if (!user) {
throw new Error(`User not found: ${id}`);
}
return user.name;
}
React 관련 리팩토링
1. Props 타입 정의
Before:
function Button({ children, onClick, variant }) {
return <button onClick={onClick}>{children}</button>;
}
After:
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'outline';
disabled?: boolean;
className?: string;
}
export function Button({
children,
onClick,
variant = 'primary',
disabled = false,
className,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={clsx(className, variant)}
>
{children}
</button>
);
}
2. Event Handler 타입
Before:
function handleChange(e: any) {
setValue(e.target.value);
}
After:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value);
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
3. Ref 타입
Before:
const inputRef = useRef(null);
After:
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
4. Custom Hook 타입
Before:
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
return { data, loading };
}
After:
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const refetch = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
}, [url]);
{ data, loading, error, refetch };
}
{ data } = useApi<>();
API 응답 타입 정의
1. Zod를 사용한 런타임 검증 (권장)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().optional(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await api.get(`users/${id}`).json();
return UserSchema.parse(response);
}
2. 수동 타입 정의
export interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
export interface Page<T> {
content: T[];
pageable: Pageable;
totalElements: number;
totalPages: number;
last: boolean;
first: boolean;
size: number;
number: number;
numberOfElements: number;
empty: boolean;
}
export interface Pageable {
pageNumber: number;
pageSize: number;
sort: Sort;
offset: number;
paged: boolean;
unpaged: boolean;
}
export interface Sort {
sorted: boolean;
unsorted: boolean;
empty: boolean;
}
유틸리티 타입 활용
1. Partial - 모든 속성을 선택적으로
interface User {
id: string;
name: string;
email: string;
}
function updateUser(id: string, updates: Partial<User>) {
}
updateUser('1', { name: 'John' });
updateUser('1', { email: 'john@example.com' });
2. Required - 모든 속성을 필수로
interface UserForm {
name?: string;
email?: string;
}
function validateUser(user: Required<UserForm>) {
}
3. Pick - 특정 속성만 선택
interface User {
id: string;
name: string;
email: string;
password: string;
}
type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
4. Omit - 특정 속성 제외
interface User {
id: string;
name: string;
password: string;
}
type UserWithoutPassword = Omit<User, 'password'>;
interface CreateUserRequest extends Omit<User, 'id'> {
}
5. Record - 키-값 타입 정의
type Status = 'active' | 'inactive' | 'pending';
const statusLabels: Record<Status, string> = {
active: '활성',
inactive: '비활성',
pending: '대기중',
};
6. Extract & Exclude
type T1 = 'a' | 'b' | 'c';
type T2 = Extract<T1, 'a' | 'b'>;
type T3 = Exclude<T1, 'a' | 'b'>;
7. ReturnType - 함수 반환 타입 추출
function getUser() {
return { id: '1', name: 'John' };
}
type User = ReturnType<typeof getUser>;
8. Parameters - 함수 파라미터 타입 추출
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
고급 패턴
1. Branded Types - 타입 안전성 강화
type UserId = string & { readonly __brand: 'UserId' };
type Email = string & { readonly __brand: 'Email' };
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
return email as Email;
}
function sendEmail(to: Email, from: Email) {
}
const userId = createUserId('123');
const email = createEmail('test@example.com');
sendEmail(email, email);
2. Template Literal Types
type Color = 'red' | 'blue' | 'green';
type Size = 'sm' | 'md' | 'lg';
type ButtonClass = `btn-${Color}-${Size}`;
3. Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
4. Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>;
type B = Unwrap<number>;
5. Type Predicates (타입 서술어)
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
function processValue(value: unknown) {
if (isUser(value)) {
console.log(value.name);
}
}
React Query 타입 패턴
import { useQuery, useMutation } from '@tanstack/react-query';
import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { Resource, CreateResourceRequest } from '@/types/resource';
import type { Page } from '@/types/api';
interface GetResourcesParams {
search?: string;
page?: number;
size?: number;
}
export const useGetResources = (
params: GetResourcesParams = {}
): UseQueryResult<Page<Resource>, Error> => {
return useQuery({
queryKey: ['resources', params],
queryFn: () =>
api
.get('resources', { searchParams: params })
.json<Page<>>(),
});
};
useCreateResource = (): <
,
,
> => {
({
:
api.(, { : data }).<>(),
});
};
타입 에러 해결 패턴
1. Object is possibly 'null' or 'undefined'
const name = user.name;
const name = user?.name;
if (user) {
const name = user.name;
}
2. Type 'X' is not assignable to type 'Y'
const status: 'active' | 'inactive' = getStatus();
const status = getStatus() as 'active' | 'inactive';
function isValidStatus(value: string): value is 'active' | 'inactive' {
return value === 'active' || value === 'inactive';
}
3. Property 'X' does not exist on type 'Y'
const data = response.data;
if (isApiResponse(response)) {
const data = response.data;
}
ESLint 규칙과 TypeScript
function process(data: any) {}
function process<T>(data: T) {}
const unused = 'value';
const _intentionallyUnused = 'value';
function getUser(): User {
return { id: '1', name: 'John' };
}
리팩토링 체크리스트
도구 및 명령어
npm run type-check
tsc --noEmit
npm run lint
npm run lint -- --fix
참고 자료