타입스크립트는 변수의 초기값을 기반으로 타입을 자동으로 추론한다.
any 타입으로 추론 (암묵적 any, 권장 ❌)let a = 10; // number 타입으로 추론
let b = "hello"; // string 타입으로 추론
let c = {
id: 1,
name: "ttining",
profile: {
nickname: "ttining._.",
},
urls: ["https://github.com/ttining-real"],
};
// 구조 분해 할당 시에도 타입 추론이 적용됨
let { id, name, profile } = c;
let [one, two, three] = [1, "hello", true];
// one: number, two: string, three: boolean으로 추론됨
let 🆚 const (타입 넓히기 포함)let 키워드와 타입 넓히기let으로 선언한 변수는 타입스크립트가 보다 범용적인 타입으로 넓혀서 추론한다.
// 초기값이 없을 경우 → 암묵적 any (타입 진화 가능)
let d; // d: any
d = 10; // d: number 타입으로 진화
d.toFixed();
d = "hello"; // d: string 타입으로 진화
d.toUpperCase();
// 초기값이 있는 경우 → 타입 넓히기 적용
let num = 10; // number 타입으로 추론됨 (10이 아닌 number)
let str = "hello"; // string 타입으로 추론됨
📌 타입 넓히기 (Widening)
- 타입스크립트는 변수를 더 범용적으로 사용할 수 있도록 타입을 넓혀서 추론한다.
let으로 선언한 경우10→ number,"hello"→ string처럼 넓은 타입으로 변환된다.- 하지만
const로 선언하면 리터럴 타입이 유지된다.
const 키워드와 리터럴 타입const로 선언하면 값이 변경될 수 없으므로, 해당 값의 리터럴 타입을 유지한다.
const num = 10; // 10 (리터럴 타입)
const str = "hello"; // "hello" (리터럴 타입)
즉, const를 사용하면 해당 값 그대로의 타입을 갖는다.
이와 다르게 let을 사용하면 더 넓은 타입으로 추론된다.
let num1 = 10; // number
const num2 = 10; // 10 (리터럴)
any 타입의 진화 (Evolution of any)any타입스크립트에서는 초기값이 없는 변수는 any로 추론되며, 이후 할당된 값에 따라 타입이 진화한다.
let x; // any
x = 10; // number
x.toFixed();
x = "hello"; // string
x.toUpperCase();
any그러나 명시적으로 any를 선언한 경우에는 진화가 발생하지 않는다.
let y: any;
y = 10; // any 유지
y = "hello"; // any 유지
any와 명시적 anyany는 타입 진화를 거치지만, 명시적인 any는 항상 any로 유지된다.any는 권장되지 않는다. → noImplicitAny 설정 활성화 권장배열과 객체도 타입스크립트가 자동으로 타입을 추론한다.
초기값을 배열처럼 다양한 타입의 값을 담을 수 있는 값으로 할당하면,
타입스크립트가 모든 배열 요소들의 타입을 비교해서 최적의 공통 타입으로 타입을 추론한다.
let arr = [1, "hello", true]; // (string | number | boolean)[] Union 타입으로 추론됨
객체의 경우도 내부 속성들의 타입을 기반으로 추론한다.
let obj = {
name: "Alice",
age: 25,
};
let { name, age } = obj;
// name: string, age: number로 추론됨
타입스크립트는 함수의 반환값도 자동으로 추론한다.
function greet() {
return "Hello"; // string으로 추론됨
}
function add(x: number, y: number) {
return x + y; // number로 추론됨
}
void 반환 타입도 추론된다.
function logMessage(): void {
console.log("Hello");
}
하지만, 명시적으로 반환 타입을 지정하는 것이 더 명확하고 안전하다. (특히, API 응답 타입이 변할 가능성이 있는 경우)
function fetchUser(): { id: number; name: string } {
return { id: 1, name: "Alice" };
}
타입 단언은 개발자가 타입스크립트의 타입 추론을 무시하고 특정 타입으로 강제하는 방법이다.
그러나 이는 타입스크립트의 타입 검사 기능을 우회하는 것으로, 값이 기대한 타입과 다를 경우 런타임 오류가 발생할 수 있다.
값 as 단언 # 단언식
A as B 형태로 사용하며, A가 B의 슈퍼타입이거나 서브타입이어야 한다.let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
type Person = {
name: string;
age: number;
};
// 빈 객체를 Person 타입으로 단언
let person = {} as Person;
person.name = "ttining";
person.age = 100;
// 초과 프로퍼티가 있는 객체를 다른 타입으로 단언
type Dog = {
name: string;
color: string;
};
let dog = {
name: "꽝꽝이",
color: "brown",
breed: "진도",
} as Dog;
⚠️ 주의
타입 단언을 남용하면 TypeScript의 타입 안전성을 해칠 수 있다.
가능한 한 타입 가드나 타입 좁히기 방법을 사용하고, 타입 단언은 정말 필요할 때만 사용하는 것이 좋다.
const 단언 (as const)const 단언을 사용하면 객체의 프로퍼티를 읽기 전용(readonly)으로 만들고, 값의 타입을 리터럴 타입으로 고정할 수 있다.let num4 = 10 as const; // number 리터럴 타입 (10)
let cat = {
name: "야옹이",
color: "yellow",
} as const;
// cat.name을 변경하려고 하면 오류 발생
cat.name = "두식이"; // ❌ 오류 발생: 읽기 전용 속성
!)값이 null 또는 undefined가 아님을 보장할 때 Non-null 단언 연산자 !를 사용한다.
단, 실제로 null이나 undefined인 경우 런타임 오류가 발생할 수 있다.
type Post = {
title: string;
author?: string;
};
let post: Post = {
title: "게시글1",
author: "ttining",
};
const length: number = post.author?.length; // number | undefined
// author가 존재한다고 확신하는 경우
const len: number = post.author!.length; // author! -> null이 아님을 단언
⚠️ 주의
!연산자를 사용하면 TypeScript의 타입 검사를 우회하므로, 값이 실제로null일 경우 런타임 오류가 발생할 수 있다.
가급적이면if문을 사용해null체크를 수행하는 것이 더 안전하다.if (post.author) { const len = post.author.length; }
타입 좁히기는 조건문 등을 사용하여 넓은 타입에서 좁은 타입으로 제한하는 기법이다.
이를 통해 TypeScript가 해당 값에 대해 더 정확한 타입을 추론할 수 있도록 도와준다.
type Person = {
name: string;
age: number;
};
// value => number : toFixed
// value => string : toUpperCase
// value => Date : getTime
// value => PErson : name은 age살 입니다.
function func(value: number | string | Date | null | Person) {
if (typeof value === "number") {
console.log(value.toFixed());
} else if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (value instanceof Date) {
console.log(value.getTime());
} else if (value && "age" in value) {
console.log(`${value.name}은 ${value.age}살 입니다.`);
}
}
typeof, instanceof, in을 이용한 좁히기typeof 연산자를 사용한 타입 좁히기typeof를 사용하면 기본 타입(string, number, boolean, symbol, bigint, undefined, object, function)을 구분할 수 있다.
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length);
} else {
console.log(value.toFixed());
}
}
instanceof 연산자를 사용한 타입 좁히기instanceof를 사용하면 객체가 특정 클래스의 인스턴스인지 확인할 수 있다.
function printDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.getTime());
} else {
console.log(value.toUpperCase());
}
}
in 연산자를 사용한 타입 좁히기in 연산자는 객체가 특정 속성을 가지고 있는지 확인하는 방법으로 타입을 좁힐 수 있다.
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(animal: Cat | Dog) {
if ("meow" in animal) {
animal.meow();
} else {
animal.bark();
}
}
typeof, instanceof, in 연산자로 좁히기 어려운 경우, 직접 프로퍼티를 검사하는 방식을 사용할 수 있다.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ((animal as Fish).swim) {
(animal as Fish).swim();
} else {
(animal as Bird).fly();
}
}
⚠️ 주의
위 방법은 타입 단언(
as
)을 사용하므로, 타입 안전성이 떨어질 위험이 있다.
대신 사용자 정의 타입 가드를 사용하는 것이 더 안전한 방법이다.
is 연산자)is 연산자를 사용하여 안전한 타입 좁히기 함수를 직접 정의할 수 있다.
type Car = { drive: () => void };
type Bike = { ride: () => void };
function isCar(vehicle: Car | Bike): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}
function useVehicle(vehicle: Car | Bike) {
if (isCar(vehicle)) {
vehicle.drive();
} else {
vehicle.ride();
}
}
✅ 장점
isCar함수는vehicle이Car타입인지 안전하게 확인해주므로,as를 남발하지 않아도 된다.
이를 통해 더 안전한 타입 좁히기가 가능해진다.
| 구분 | 설명 |
|---|---|
typeof | 기본 타입 (string, number 등) 구분 |
instanceof | 특정 클래스의 인스턴스인지 확인 |
in | 객체가 특정 프로퍼티를 가지고 있는지 확인 |
| 명시적 타입 검사 | 강제적인 타입 단언(as)을 사용한 방법 (권장 ❌) |
사용자 정의 타입 가드(is) | 안전한 타입 좁히기 방법 (권장 ⭕) |