type Person = {
name: string;
age: number;
};
function printPerson(person: Person) {
for(let key in person) {
console.log(`${key} : ${person[key]}`);
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Person'.
// ❌ 에러: 'string' 타입은 'Person'에 인덱스로 사용할 수 없습니다.
}
}
/**
* for...in에서 key의 타입은 항상 string입니다.
* 하지만 Person의 키 타입은 keyof Person("name" | "age")입니다.
* person[key]를 사용하면 TypeScript는 key가 "name" 또는 "age"가 아닐 수도 있다고 판단하여 에러를 발생시킵니다.
*/
for...in 구문에서 key는 항상 string 타입이다.
반면, Person 타입의 키는 "name" | "age" (keyof Person)이다.
따라서 person[key]에서 TypeScript는 key가 "name"이나 "age"가 아닐 가능성을 고려하여 오류를 발생시킨다.
const personWithExtraProps = {
name: "Alice",
age: 30,
address: "Seoul", // ❌ Person 타입에는 없는 속성
};
printPerson(personWithExtraProps);
(1) as keyof Person으로 캐스팅
TypeScript에게 key가 반드시 Person 타입의 키("name" | "age")임을 명시적으로 알려주면 됨.
function printPerson(person: Person) {
for (let key in person) {
console.log(`${key} : ${person[key as keyof Person]}`);
}
}
(2) Object.keys()와 as keyof Person 사용
Object.keys()를 사용하면 실제로 존재하는 키만 가져올 수 있음
function printPerson(person: Person) {
Object.keys(person).forEach((key) => {
console.log(`${key} : ${person[key as keyof Person]}`);
});
}
(3) Object.entries() 사용
Object.entries()를 사용하면 키와 값을 동시에 안전하게 가져올 수 있습니다.
function printPerson(person: Person) {
Object.entries(person).forEach(([key, value]) => {
console.log(`${key} : ${value}`);
});
}
/**
* Object.entries(person)의 반환 타입은 **[string, any][]**입니다.
* 즉, 모든 키는 string 타입, 모든 값은 any 타입이 됩니다.
* 따라서 key가 "name" | "age"가 아닌 일반적인 문자열(string)로 처리되므로, TypeScript가 타입 오류를 발생시키지 않습니다.
* 또한 value도 any 타입이므로, console.log(value)에서도 타입 검사 없이 문제없이 실행됩니다
**/
Object.entries()의 반환 타입이 [string, any][]이기 때문에 타입 오류 없이 동작합니다.
단점: Person 타입 외의 속성(ex. address)도 출력될 수 있습니다.
const personWithExtraProps = {
name: "Alice",
age: 30,
address: "Seoul", // ❌ Person 타입에 없는 속성
};
printPerson(personWithExtraProps);
(1) 방법 1: 하드코딩된 키 배열 사용
// 방법 1: 하드코딩
function printPersonStrict(person: Person) {
(["name", "age"] as const).forEach((key) => {
console.log(`${key} : ${person[key]}`);
});
}
(2) 제네릭 + 전달된 키 배열 활용
// 방법 2: 제네릭과 전달된 키 배열 활용
function printPersonSafe<T extends Record<string, any>>(person: T, keys: (keyof T)[]) {
keys.forEach((key) => {
console.log(`${key} : ${person[key]}`);
});
}
(3) 방법 3: Object.keys 사용 + 타입 단언 조합
//방법 3: Object.keys 사용 + 단언
function printPersonStrict(person: Person) {
(Object.keys(person) as (keyof Person)[]).forEach((key) => {
console.log(`${key} : ${person[key]}`);
});
}
(4) keyof를 기반으로 타입 안전한 키 배열 헬퍼
function typedKeys<T>() {
return <K extends keyof T>(...keys: K[]) => keys;
}
// 2. 사용 예시
const personKeys = typedKeys<Person>()("name", "age"); // personKeys는 ('name' | 'age')[]
function printPerson(person: Person) {
personKeys.forEach((key) => {
console.log(`${key} : ${person[key]}`);
});
}
function log(message) { // ❌ 암시적 any, noImplicitAny가 true이면 에러
console.log(message);
}
(1) Object
let obj: Object = {
toString() {
return false; // 'boolean'은 'string'에 할당할 수 없음.
}
};
(2) object
let a: object;
a = { name: "Alice" }; // ✅ OK
a = [1, 2, 3]; // ✅ OK
a = () => {}; // ✅ OK
a = 42; // ❌ Error
a = "Hello"; // ❌ Error
(3) {} 타입
let b: {};
b = 42; // ✅ OK
b = "hello"; // ✅ OK
b = { x: 1 }; // ✅ OK
b = null; // ❌ Error
b = undefined; // ❌ Error
JavaScript에서는 데이터를 객체({ name: "Stefan", age: 40 })나 배열(["Stefan", 40])로 저장할 수 있음.
객체는 키-값 구조로 이해하기 쉽고, 배열은 구조적 제약이 적어 이름을 자유롭게 할당할 수 있음.
TypeScript는 배열(["Stefan", 40])을 자동으로 (string | number)[]로 추론함.
즉, 배열의 요소가 문자열이든 숫자든 상관없이 추가적인 요소도 허용되는 문제가 생김.
구조적 제약이 없어서 const [name, age] = person;을 해도 name과 age의 타입이 string | number가 되어 불편함.
튜플은 요소의 개수와 타입을 고정할 수 있음.
const person: [string, number] = ["Stefan", 40];
person[0]은 string, person[1]은 number로 고정됨.
길이가 2개로 고정되어 추가적인 요소를 넣을 수 없음 (person[2] = false; → ❌ 오류 발생).
더 명확한 코드 작성 가능.
type Person = [name: string, age: number];
가독성이 좋아지고, 협업할 때 코드 이해가 쉬워짐. 함수의 매개변수에서도 활용 가능
//튜플 없이 사용
function hello(name: string, msg: string): void {}
//튜플 사용
function hello(...args: [name: string, msg: string]): void {}
function h(a: string, b: string, c: string): void {}
function h(a: string, ...r: [string]): void {}
// 인터페이스 병합 가능
interface Person {
name: string;
}
interface Person {
age: number;
}
// { name: string; age: number; }으로 병합됨
interface FormData {
name: string;
age: number;
address: string[];
}
function send(data: FormData) {
console.log(data.entries()); // ✅ 타입상 OK
}
type FormData = {
name: string;
age: number;
address: string[];
};
// ❌ Duplicate identifier 'FormData'.(2300)
→ 이처럼 type alias는 동일 이름으로의 병합이 불가능하기 때문에,
의도치 않게 DOM의 FormData와 충돌할 경우 컴파일 타임에 문제를 알려줘서
실수를 미리 방지할 수 있다.
외부에서 확장될 수 있는 공개 API나 라이브러리용 타입은 interface를 사용
내부에서만 사용하는 타입, 특히 전역 이름과 충돌 가능성이 있는 타입은 type alias로 정의
→ 타입 병합을 방지하고 의도치 않은 확장을 막을 수 있음
function task(name: string, dependencies: string[]): void;
function task(name: string, callback: CallbackFn): void;
function task(name: string, dependencies: string[], callback: CallbackFn): void;
// 실제 구현부
function task(name: string, param2: string[] | CallbackFn, param3?: CallbackFn): void {
// ...
}
task 함수에서 허용된 오버로드 목록
task(name: string, callback: CallbackFn): void;
task(name: string, dependencies: string[], callback: CallbackFn): void;
타입으로 표현하면
type TaskFn = {
(name: string, dependencies: string[]): void;
(name: string, callback: CallbackFn): void;
(name: string, dependencies: string[], callback: CallbackFn): void;
};
//실제 구현정의는 필요없이 형식 시스템 오버로드만 있으면 된다.
const task: TaskFn =( name : string, dependencies: string[] | CallbackFn, callback?: CallbackFn) => {
//
}
const button = document.querySelector("button");
button?.addEventListener("click", handleToggle);
function handleToggle(this: HTMLButtonElement) { // this가 HTMLButtonElement임을 명시
this.classList.toggle("clicked");
}
//또는 상위 타입으로 유용성 개선
function handleToggle(this: HTMLElement) { // 모든 HTML 요소에 적용 가능
this.classList.toggle("clicked");
}
function handleToggle(this: HTMLElement) {
this.classList.toggle("clicked");
}
type ToggleFn = typeof handleToggle;
// (this: HTMLElement) => void
function handleToggle(this: HTMLButtonElement) {
this.classList.toggle("clicked");
}
type ToggleFn = typeof handleToggle;
type WithoutThis = OmitThisParameter<ToggleFn>;
// () => void
const toggleFnWithoutThis: WithoutThis = handleToggle.bind(button);
// bind(button)을 사용하면 this가 사라지므로 타입이 () => void가 됨.
type ToggleFn = typeof handleToggle;
// ToggleFn의 타입: (this: HTMLButtonElement) => void
// type ToggleFn = (this: HTMLButtonElement) => void;
type ToggleFnThis = ThisParameterType<ToggleFn>;
// ToggleFnThis = HTMLButtonElement
const sym1 = Symbol("description");
const sym2 = Symbol("description");
console.log(sym1 === sym2); // ❌ false (항상 새로운 심볼 생성)
const secretKey = Symbol("secret");
const user = {
name: "Alice",
age: 30,
[secretKey]: "This is a secret value"
};
console.log(JSON.stringify(user));
// 출력: {"name":"Alice","age":30} (Symbol 속성 제외)
console.log(user[secretKey]);
// 출력: "This is a secret value" (여전히 접근 가능)
전역 레지스트리에 등록된 심볼을 공유
동일한 키로 만든 심볼은 동일한 참조를 가짐
const symA = Symbol.for("sharedKey");
const symB = Symbol.for("sharedKey");
console.log(symA === symB); // true (전역 심볼은 공유됨)
```
전역 심볼에 등록된 심볼에서 키를 가져오는 함수
const usedSymbolKeys: string[] = [];
function extendObject(obj: any, symbol: symbol, value: any) {
// 전역 심볼의 키를 가져옴
const key = Symbol.keyFor(symbol);
// 처음 본 키라면 저장
if (key && !usedSymbolKeys.includes(key)) {
usedSymbolKeys.push(key);
}
// 객체에 해당 심볼을 키로 추가
obj[symbol] = value;
}
function printAllValues(obj: any) {
//usedSymbolKeys에 저장된 키들을 Symbol.for(key)로 다시 변환하여 값을 출력함.
//즉, Symbol.for()를 사용하면 같은 키를 가진 심볼을 언제든지 다시 찾아서 사용할 수 있음.
usedSymbolKeys.forEach(key => {
console.log(obj[Symbol.for(key)]);
});
}
Ts 특징(구조적 타이핑)에 따른 문제점
//TypeScript는 원래 구조적 타이핑(Structural Typing, Duck Typing) 사용
type ID = string;
type UserID = string;
let userA: ID = "user123";
let userB: UserID = "user456";
userA = userB; // ✅ 가능 (둘 다 string 타입이므로)
unique symbol을 사용하면 TypeScript가 명목적 타입처럼 동작
//unique symbol을 사용하면 TypeScript가 명목적 타입처럼 동작
const USER_ID: unique symbol = Symbol("USER_ID");
const ORDER_ID: unique symbol = Symbol("ORDER_ID");
type UserId = typeof USER_ID;
type OrderId = typeof ORDER_ID;
let userId: UserId = USER_ID;
let orderId: OrderId = ORDER_ID;
userId = orderId; // ❌ 오류 발생! (UserId와 OrderId는 다른 타입)
런타임시 명목상 형식 검사를 하는데 필요한 형식이다?
const DEV_MODE: unique symbol = Symbol("dev");
const PROD_MODE: unique symbol = Symbol("prod");
type Mode = typeof DEV_MODE | typeof PROD_MODE;
function startApp(mode: Mode) {
if (mode === DEV_MODE) {
console.log("Starting in Development Mode...");
} else if (mode === PROD_MODE) {
console.log("Starting in Production Mode...");
}
}
startApp(DEV_MODE); // 정상 작동
startApp(PROD_MODE); // 정상 작동
startApp(Symbol("dev")); // 오류 발생 (잘못된 `Symbol`)
구조적 타입 검사란?
명목적 타입 검사란?
명목상 형식 검사가 필요한 이유
unique symbol을 사용하면 "특정한 심볼 값만 허용하는" 명목적 타입 검사를 런타임에서도 수행 가능
//unique symbol을 사용한 명목적 타입 검사의 예시
declare const userIdBrand: unique symbol;
declare const productIdBrand: unique symbol;
type UserId = number & { [userIdBrand]: void }; //intersection type , number이면서 userIdBrand라는 키를 가진 객체 형태의 타입
type ProductId = number & { [productIdBrand]: void };
function getUser(id: UserId) {}
function getProduct(id: ProductId) {}
const uid = 123 as UserId;
const pid = 123 as ProductId;
getUser(uid); // ✅ OK
getUser(pid); // ❌ 에러! → 명확히 다른 타입으로 구분됨
// TypeScript 타입 공간에만 존재
type Collection = string[];
// JavaScript 값 공간에서 실행됨
function printCollection(coll: Collection) {
console.log(...coll);
}
const person = { name: "Stefan" };
type PersonType = typeof person;
const anotherPerson: PersonType = { name: "Alice" };
class Person {
name: string;
constructor(n: string) {
this.name = n;
}
}
// 클래스는 값으로 사용 가능
const personInstance = new Person("Stefan");
// 클래스는 타입으로도 사용 가능
type PersonType = Person;
const anotherPerson: PersonType = new Person("Alice");
function checkPerson(person: Person) {
return person instanceof Person;
}
checkPerson(new Person("Stefan")); // true
checkPerson({ name: "Stefan" }); // false (Person의 인스턴스가 아님)
type PersonProps = { name: string };
function Person({ name }: PersonProps) {
// React 컴포넌트
}
type PrintComponentProps = {
// collection: Person[]; //❌ 'Person' 은 값을 참조하지만 형식으로 사용되고 있습니다.
collection: PersonProps[];
};