Type Guards(타입가드)
Type Guards(타입가드)는 타입스크립트에서 변수의 타입을 보다 구체적으로 유추할 수 있도록 하는 기능이다. 이를 통해 코드의 가독성과 안정성을 높일 수 있다. Type Guard에는 다음과 같은 종류가 있다.
typeof Type Guards
typeof Type Guards는 JavaScript의 typeof 연산자를 사용하여 변수의 타입을 검사하는 것이다. 예를 들어, 다음과 같이 사용할 수 있다.
function printValue(value: string | number) {
if (typeof value === 'string') {
console.log(`Value is a string with length ${value.length}`);
} else {
console.log(`Value is a number`);
}
}
위의 코드에서는 typeof 연산자를 사용하여 value의 타입이 문자열인지 검사하고 있다. 이렇게 typeof Type Guards를 사용하면, 코드의 가독성이 높아지고, 타입 검사를 간편하게 할 수 있다.
typeof Type Guards를 사용하면 문자열, 숫자, 불리언, undefined 등의 원시 타입을 검사할 수 있다. 하지만 객체나 배열 등의 복합 타입은 검사할 수 없다.
이 경우, User-Defined Type Guards를 사용해야 한다.
instanceof Type Guards
instanceof Type Guards는 JavaScript의 instanceof 연산자를 사용하여 변수의 타입을 검사하는 것이다. instanceof 연산자는 객체의 생성자(constructor) 함수와 비교하여, 객체가 해당 생성자 함수로부터 생성된 것인지를 검사한다.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
function printAnimalInfo(animal: Animal) {
if (animal instanceof Cat) {
console.log(`This is a cat with breed ${animal.breed}`);
} else {
console.log(`This is an animal with name ${animal.name}`);
}
}
위의 코드에서는 instanceof 연산자를 사용하여 animal이 Cat 클래스로부터 생성된 객체인지를 검사하고 있다. 이렇게 instanceof Type Guards를 사용하면, 객체의 타입을 검사할 수 있다.
instanceof Type Guards를 사용하면, 클래스의 상속 관계에서도 사용할 수 있다.
만약 검사 대상이 클래스가 아닌 인터페이스나 Type Aliases 등의 타입이라면, instanceof Type Guards를 사용할 수 없다.
이 경우, User-Defined Type Guards를 사용해야 한다.
in Type Guards
in Type Guards는 JavaScript의 in 연산자를 사용하여 객체가 특정 프로퍼티를 가지고 있는지를 검사하는 것이다.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function printShapeInfo(shape: Shape) {
if ("radius" in shape) {
console.log(`This is a circle with radius ${shape.radius}`);
} else {
console.log(`This is a square with side length ${shape.sideLength}`);
}
}
위의 코드에서는 in 연산자를 사용하여 shape 객체가 Circle 인터페이스의 radius 프로퍼티를 가지고 있는지를 검사하고 있다. 이렇게 in Type Guards를 사용하면, 객체가 특정 프로퍼티를 가지고 있는지 검사할 수 있다.
in Type Guards를 사용하면, 인터페이스나 객체 타입 등의 복합 타입에서 프로퍼티의 존재 여부를 검사할 수 있다.
하지만 프로퍼티의 타입을 검사할 수는 없다.
이 경우, User-Defined Type Guards를 사용해야 한다.
User-Defined Type Guards
User-Defined Type Guards는 사용자가 직접 함수를 정의하여 변수의 타입을 검사하는 것이다. 이를 사용하면, 타입스크립트가 변수의 타입을 추론하지 못하는 경우에도 사용자가 직접 정의한 함수를 통해 변수의 타입을 검사할 수 있다.
예를 들어, 다음과 같이 isString이라는 함수를 정의하여 문자열인지 아닌지를 검사하는 User-Defined Type Guard를 만들 수 있다.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
위의 함수에서는 value의 타입이 unknown인지 검사한 후, value가 문자열인 경우 true를 반환한다. 이 함수를 사용하면, 변수가 문자열인지 아닌지를 검사할 수 있다.
function printLength(value: string | number) {
if (isString(value)) {
console.log(`Length of string is ${value.length}`);
} else {
console.log(`Value is a number`);
}
}
위의 코드에서는 isString 함수를 사용하여 value의 타입이 문자열인지 검사하고 있다. 이렇게 User-Defined Type Guard를 사용하면, 코드의 가독성과 안정성을 높일 수 있다.
Type Assertion(타입 단언)
Type Assertion(타입 단언)은 타입스크립트에서 변수나 표현식의 타입을 개발자가 수동으로 지정하는 것을 말한다. Type Assertion은 컴파일러에게 해당 변수나 표현식의 타입이 무엇인지 알려줌으로써 개발자가 코드의 타입 안정성을 높이고, 자동완성 등의 장점을 활용할 수 있도록 한다.
Type Assertion은 두 가지 방법으로 사용할 수 있다.
Angle Bracket Syntax
Angle Bracket Syntax는 변수나 표현식 뒤에 "<>" 기호를 사용하여 타입 Assertion을 하는 방법이다.
let someValue: any = "hello";
let strLength: number = (<string>someValue).length;
위 코드에서, someValue 변수는 any 타입으로 지정되어 있으므로, 컴파일러는 해당 변수의 타입을 인식할 수 없습니다. 하지만, "<>" 기호를 사용하여 someValue 변수를 string 타입으로 Assertion 하면, 컴파일러는 해당 변수의 타입을 string으로 인식한다.
as 키워드
as 키워드는 변수나 표현식 뒤에 "as" 키워드를 사용하여 타입 Assertion을 하는 방법이다.
let someValue: any = "hello";
let strLength: number = (someValue as string).length;
Angle Bracket Syntax와 마찬가지로, as 키워드를 사용하여 someValue 변수를 string 타입으로 Assertion 하면, 컴파일러는 해당 변수의 타입을 string으로 인식한다.
두 가지 방법 모두 같은 결과를 반환하지만, 일반적으로 as 키워드를 사용하는 것이 가독성 면에서 더 좋다.
Angle Bracket Syntax는 JSX 코드에서 혼동을 야기할 수 있기 때문이다.
하지만, Type Assertion은 타입 안정성을 제공하는 것이 아니다. 개발자가 잘못된 타입으로 Assertion을 하거나, 실제 타입과 일치하지 않는 타입을 Assertion 하는 경우에는 런타임 에러가 발생할 수 있다. 따라서, Type Assertion은 주의해서 사용해야 한다.
Type Assertion은 다음과 같은 상황에서 유용하게 사용된다.
- any 타입을 명시적으로 타입으로 변환할 때 any 타입은 컴파일러가 타입을 추론하지 못할 때 사용된다. any 타입은 코드의 유연성을 높여주지만, 타입 안정성을 저하시키는 요소이다. 따라서, any 타입을 사용하는 경우에는 Type Assertion을 사용하여 명시적으로 타입을 변환해야 한다.
- 타입 호환성을 보장하기 위해 TypeScript는 구조적 타이핑(structural typing)을 사용하여 타입 호환성을 검사한다. 따라서, 다른 타입에서 유래된 객체나 인터페이스를 사용할 때, Type Assertion을 사용하여 호환되는 타입으로 변환해야 한다.
- 다양한 타입으로부터 유래된 유니온 타입을 다룰 때 TypeScript에서 유니온 타입은 여러 가지 타입 중에서 하나일 수 있다. 따라서, 유니온 타입을 다룰 때, 타입스크립트는 해당 타입이 유니온 타입 내의 어떤 타입인지 알 수 없다. 이 경우에는 Type Assertion을 사용하여 해당 변수 또는 표현식의 타입을 명시적으로 지정할 수 있다.
이러한 상황에서 Type Assertion을 사용할 때에는, 타입이 실제로 해당하는 타입과 일치하는지, 또는 호환 가능한지 검사해야 한다. Type Assertion을 남발하면 타입 안정성을 저하시킬 수 있다.
Type Aliases(타입 별칭)
타입스크립트(TypeScript) 공식문서에서 타입 별칭(Type Aliases)이란 다음과 같이 정의된다.
Type Aliases allow you to create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you’d otherwise have to write by hand.
Type Aliases을 사용하면 타입의 새 이름을 만들 수 있다. Type Aliases은 때때로 인터페이스와 유사하지만 Primitives, Unions, Tuples 및 수동으로 작성해야 하는 기타 타입의 이름을 지정할 수 있다.
Type Aliases(타입 별칭)은 TypeScript에서 타입을 정의할 때 사용하는 문법 중 하나로, 기존에 존재하는 타입에 새로운 이름을 부여하여 코드의 가독성과 유지보수성을 높일 수 있다.
Type Aliases은 type 키워드를 사용하여 정의할 수 있으며, 다음과 같은 형태로 작성된다.
type TypeName = TypeExpression;
여기서 TypeName은 새로운 타입의 이름을 의미하며, TypeExpression은 기존에 존재하는 타입, 또는 타입을 정의하는 표현식을 의미한다.
type Person = {
name: string;
age: number;
};
위 예시에서 Person은 새로운 타입의 이름으로 name과 age 속성을 가진 객체 타입이다. 이후 코드에서 Person 타입을 사용하여 변수를 선언하거나, 함수의 인자나 반환값의 타입으로 지정할 수 있다.
Type Aliases은 코드의 가독성을 높이기 위해, 복잡한 타입을 단순한 이름으로 대체할 수 있는 경우에 유용하게 사용된다.
예를 들어, 다음과 같이 복잡한 함수 시그니처에 대한 Type Aliases을 정의할 수 있다.
type SearchFunc = (source: string, subString: string) => boolean;
위 예시에서 SearchFunc는 (source: string, subString: string) => boolean 타입을 대체하는 이름으로, 함수 시그니처를 더 간결하고 가독성 높은 형태로 사용할 수 있다.
Type Aliases은 기존 타입을 새로운 이름으로 대체하는 역할을 하기 때문에, 타입을 정의할 때 반복되는 코드를 줄일 수 있으며, 코드의 유지보수성을 높일 수 있다. 또한, 타입의 가독성을 높이기 위해 다양한 형태의 Type Aliases을 사용할 수 있으며, 필요한 경우 기존 Type Aliases을 수정하거나 삭제할 수도 있다.
기본타입(Primitives Types)
Type Aliases은 기본타입(Primitives Types)을 대체하는 이름으로 사용할 수 있다. 예를 들어, 다음과 같이 string 타입에 대한 Type Aliases을 정의할 수 있다.
type MyString = string;
위 예시에서 MyString은 string 타입을 대체하는 이름으로, MyString 타입으로 변수를 선언하거나 함수의 인자나 반환값의 타입으로 지정할 수 있다.
유니온타입(Unions Types)
Type Aliases은 유니온타입(Unions Types)을 대체하는 이름으로 사용할 수 있다. 예를 들어, 다음과 같이 string | number 타입에 대한 Type Aliases을 정의할 수 있다.
type MyUnion = string | number;
위 예시에서 MyUnion은 string | number 타입을 대체하는 이름으로, MyUnion 타입으로 변수를 선언하거나 함수의 인자나 반환값의 타입으로 지정할 수 있다.
튜플타입(Tuples Types)
Type Aliases은 튜플타입(Tuples Types)을 대체하는 이름으로 사용할 수 있다. 예를 들어, 다음과 같이 string[] 타입에 대한 Type Aliases을 정의할 수 있다.
type MyTuple = [string, number];
위 예시에서 MyTuple은 [string, number] 타입을 대체하는 이름으로, MyTuple 타입으로 변수를 선언하거나 함수의 인자나 반환값의 타입으로 지정할 수 있다.
Type Inference(타입 추론)
타입스크립트는 정적 타입 언어로서, 변수와 함수의 타입을 명시적으로 지정할 수 있다. 하지만 때로는 타입을 명시적으로 지정하지 않고도 변수나 함수의 타입을 추론할 수 있다. 이를 타입 추론(Type Inference)이라고 한다.
타입 추론은 변수나 함수를 사용하는 곳에서 그 타입을 유추한다. 이때 타입스크립트는 변수나 함수가 사용된 위치에서 할당된 값이나 반환값을 분석하여 타입을 추론한다.
let x = 3;
이 코드에서 타입을 명시적으로 지정하지 않았지만, 타입스크립트는 변수 x가 number 타입이라는 것을 추론할 수 있다. 이는 변수 x가 숫자 3으로 초기화되었기 때문이다.
타입스크립트의 타입 추론은 여러 가지 규칙을 따른다.
초기화 값에 따른 타입 추론
변수나 상수를 선언하면서 초기화 값을 지정하면, 타입스크립트는 초기화 값의 타입을 기반으로 변수나 상수의 타입을 추론한다.
const message = "Hello, world!";
이 코드에서 message 상수는 문자열 값을 가지므로, 타입스크립트는 message 상수의 타입을 string으로 추론한다.
리터럴 타입에 따른 타입 추론
리터럴은 값 그 자체를 나타내는 것으로, 문자열이나 숫자 등을 포함한다. 리터럴 값을 변수에 할당하면, 타입스크립트는 변수의 타입을 리터럴 타입으로 추론한다.
let x = 3;
이 코드에서 x 변수에는 number 타입의 리터럴 값인 3이 할당되므로, 타입스크립트는 x 변수의 타입을 number로 추론한다.
함수 반환 타입 추론
함수의 반환 타입은 함수가 반환하는 값의 타입에 따라 추론된다.
function add(a: number, b: number) {
return a + b;
}
이 코드에서 add 함수는 매개변수 a와 b의 합을 반환하므로, 타입스크립트는 add 함수의 반환 타입을 number로 추론한다.
타입 추론 제한
타입 추론은 모든 경우에 항상 성공하는 것은 아니다. 타입 추론이 실패하면 타입스크립트는 타입을 any로 추론한다. 이는 타입 추론이 실패했을 때 코드에서 오류가 발생하지 않도록 하는 역할을 한다.
타입 추론은 코드의 가독성과 유지보수성을 높이는 데에 큰 역할을 한다.
하지만 때로는 타입스크립트가 잘못된 타입을 추론하는 경우도 있으므로, 명시적으로 타입을 지정하는 것이 더욱 안전한 방법일 수 있다.
Keyof Operator(keyof 연산자)
객체의 속성 이름 가져오기
keyof 연산자(Keyof Operator)는 객체의 모든 속성 이름을 문자열 리터럴 유니온 타입으로 가져올 수 있는 유용한 방법이다. 즉, keyof 연산자를 사용하면 객체의 속성 이름을 문자열로 가져와 타입 안전성을 보장할 수 있다.
interface Person {
name: string;
age: number;
address: string;
}
keyof 연산자를 사용하여 Person 인터페이스의 모든 속성 이름을 가져올 수 있다.
type PersonKeys = keyof Person; // "name" | "age" | "address"
이 결과를 보면, PersonKeys는 "name", "age", "address" 중 하나일 수 있는 문자열 리터럴 유니온 타입이 된다.
이것은 객체의 속성 이름을 문자열 리터럴로 가져온 다음, 이를 사용하여 다른 곳에서 타입을 안전하게 참조할 수 있도록 한다.
예를 들어, 다음과 같은 함수를 작성할 수 있다.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
이 함수는 T 타입의 객체와 K 타입의 속성 이름을 받아 해당 객체에서 속성 값을 가져온다. 이 함수를 사용할 때, K는 T의 모든 속성 이름 중 하나일 수 있으므로, keyof 연산자를 사용하여 K 타입의 값을 제한할 수 있다.
const person: Person = { name: "John", age: 30, address: "123 Main St" };
const name: string = getProperty(person, "name"); // "John"
const age: number = getProperty(person, "age"); // 30
const address: string = getProperty(person, "address"); // "123 Main St"
const invalid = getProperty(person, "invalid"); // 컴파일 오류: 속성 "invalid"은 "Person" 타입에 존재하지 않습니다.
클래스의 메소드 이름과 정적 속성 이름 가져오기
keyof 연산자는 객체의 속성 이름 뿐만 아니라, 클래스의 메소드 이름과 정적 속성 이름도 가져올 수 있다.
class Car {
static manufacturer = "Tesla";
model: string;
year: number;
constructor(model: string, year: number) {
this.model = model;
this.year = year;
}
start() {
console.log("Starting the car...");
}
stop() {
console.log("Stopping the car...");
}
}
이제 keyof 연산자를 사용하여 Car 클래스의 속성 이름과 메소드 이름을 가져올 수 있다.
type CarKeys = keyof Car; // "manufacturer" | "model" | "year" | "start" | "stop"
이 결과를 보면, CarKeys는 "manufacturer", "model", "year", "start", "stop" 중 하나일 수 있는 문자열 리터럴 유니온 타입이 된다.
제네릭 타입 제한하기
또한 keyof 연산자를 사용하여 제네릭 타입을 제한할 수도 있다.
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
이 함수는 T 타입의 객체와 K 타입의 속성 이름을 받아 해당 객체에서 속성 값을 가져온다. K는 T의 속성 이름 중 하나일 수 있다.따라서, getValue 함수를 호출할 때, K에 T의 모든 속성 이름 중 하나를 전달할 수 있다.
const car = new Car("Model S", 2022);
const manufacturer: string = getValue(Car, "manufacturer"); // "Tesla"
const model: string = getValue(car, "model"); // "Model S"
const year: number = getValue(car, "year"); // 2022
const start: () => void = getValue(car, "start"); // () => console.log("Starting the car...")
const stop: () => void = getValue(car, "stop"); // () => console.log("Stopping the car...")
const invalid = getValue(car, "invalid"); // 컴파일 오류: 속성 "invalid"은 "Car" 타입에 존재하지 않습니다.