02. 데이터 모델과 함수
다루는 데이터의 모양과 함수의 입출력을 타입으로 적는 방법을 정리합니다.
객체 타입
필요한 속성과 타입을 그대로 적습니다.
function render(todo: { id: number; title: string }) {
return todo.title;
}같은 모양을 여러 곳에서 쓰면 이름을 붙입니다.
interface Todo {
id: number;
title: string;
done: boolean;
}객체 리터럴을 바로 넘기면 선언에 없는 속성은 거부됩니다. 오타를 잡아 주는 검사입니다.
const todo: Todo = {
id: 1,
title: "공부",
done: false,
titel: "오타", // 오류: 'titel'은 Todo에 없습니다
};다만 변수를 거치면 이 검사가 적용되지 않습니다. 속성이 더 많아도 필요한 것이 다 있으면 통과합니다.
const input = { id: 1, title: "공부", done: false, extra: 1 };
const todo: Todo = input; // 통과합니다type과 interface
객체 모양을 적는 데는 둘 다 쓸 수 있습니다.
interface Todo {
id: number;
}
type TodoType = {
id: number;
};차이는 확장 방식과 다룰 수 있는 범위입니다.
// interface는 선언을 합칩니다
interface Window {
myApp: string;
}
// type은 합칠 수 없습니다
type A = { a: string };
type A = { b: string }; // 오류: 중복 식별자// type만 할 수 있는 것
type Status = "todo" | "done"; // 유니온
type Pair = [number, number]; // 튜플
type Keys = keyof Todo; // 연산 결과객체 모양에는 interface, 유니온이나 연산이 필요하면 type을 쓰는 기준이면 충분합니다. 외부에 공개하는 라이브러리라면 사용자가 속성을 덧붙일 수 있는 interface가 유리합니다.
확장은 양쪽 다 됩니다.
interface Base {
id: number;
}
interface Extended extends Base {
title: string;
}
type BaseType = { id: number };
type ExtendedType = BaseType & { title: string };선택 속성
있을 수도, 없을 수도 있는 속성에는 ?를 붙입니다. 타입에 undefined가 더해집니다.
interface Todo {
id: number;
title: string;
memo?: string; // string | undefined
}
const todo: Todo = { id: 1, title: "공부" }; // memo가 없어도 됩니다
todo.memo.length; // 오류: undefined일 수 있습니다
todo.memo?.length; // number | undefinedmemo?: string과 memo: string | undefined는 다릅니다. 후자는 속성을 반드시 적어야 합니다.
interface A {
memo?: string;
}
interface B {
memo: string | undefined;
}
const a: A = {}; // 통과
const b: B = {}; // 오류: memo가 없습니다
const b2: B = { memo: undefined }; // 통과값을 빠뜨린 것과 비워 둔 것을 구분하고 싶을 때 이 차이가 쓸모 있습니다.
읽기 전용 속성
만든 뒤 바꾸지 않을 값에 표시합니다.
interface Todo {
readonly id: number;
title: string;
}
const todo: Todo = { id: 1, title: "공부" };
todo.id = 2; // 오류
todo.title = "운동"; // 가능배열에도 쓸 수 있습니다. 원본을 바꾸는 메서드가 사라집니다.
function print(list: readonly string[]) {
list.push("a"); // 오류: push는 readonly 배열에 없습니다
return [...list, "a"]; // 새 배열을 만드는 것은 됩니다
}이 표시는 컴파일 시점에만 유효합니다. 실행 중에 값이 바뀌는 것을 막지는 않습니다. 함수가 인자를 건드리지 않겠다는 약속을 드러내는 용도입니다.
함수 입출력 타입
function addTodo(list: Todo[], title: string): Todo[] {
return [...list, { id: Date.now(), title, done: false }];
}
const filter = (list: Todo[], done: boolean): Todo[] =>
list.filter((todo) => todo.done === done);콜백을 받을 때는 함수 타입을 적습니다.
type Comparator = (a: Todo, b: Todo) => number;
function sort(list: Todo[], compare: Comparator): Todo[] {
return [...list].sort(compare);
}함수를 인자로 넘기는 자리에서는 매개변수 타입을 다시 쓰지 않아도 됩니다. 기대되는 타입에서 추론됩니다.
sort(todos, (a, b) => a.id - b.id); // a와 b는 Todo로 추론됩니다선택적 매개변수와 기본값도 같은 방식입니다.
function format(todo: Todo, prefix = "- "): string {
return `${prefix}${todo.title}`;
}
function find(list: Todo[], id?: number): Todo | undefined {
return id === undefined ? list[0] : list.find((todo) => todo.id === id);
}satisfies
타입에 맞는지 검사하되 추론은 그대로 두고 싶을 때 씁니다.
타입을 직접 붙이면 값이 가진 구체적인 정보가 넓혀집니다.
type Config = Record<string, string | string[]>;
const config: Config = {
name: "todo",
tags: ["a", "b"],
};
config.tags.map((t) => t); // 오류: string | string[]에는 map이 없습니다satisfies로 바꾸면 검사는 받으면서 각 속성의 실제 타입이 유지됩니다.
const config = {
name: "todo",
tags: ["a", "b"],
} satisfies Config;
config.tags.map((t) => t); // 가능: tags는 string[]입니다
config.port = 3000; // 오류: Config에 맞지 않습니다상수 묶음을 정의할 때 특히 유용합니다.
const STATUS = {
todo: "할 일",
done: "완료",
} satisfies Record<string, string>;
type Status = keyof typeof STATUS; // "todo" | "done"