09. Advanced Types
타입 가드와 다른 타입들 (Type Guards and Differentiating Types)
- Union Type(
|
)은 값이 취할 수 있는 유형이 겹칠 때 사용할 수 있음이 때 구체적으로
Fish
를 가지고 있는지 알아야한다면 어떻게 처리할 것인가?
- 자바스크립트에서 가능한 두 가지 값을 구분하는 방법에는 멤버의 존재를 확인하는 것이 있음
앞에서 언급했듯이 union type의 모든 요소가 공통적으로 가지고 있는 속성에만 접근이 가능함
// pet: Fish | Bird
let pet = getSmallPet();
if ("swim" in pet) {
pet.swim();
}
if (pet.fly) {
// ..
}
in
을 이용해서 속성이 존재하는지 확인할 수 있지만, 직접적으로 접근하는 것은 안됨
- 위의 코드에서 접근 가능하게 하려면 Type Assertion을 활용해야 함
let pet = getSmallPet();
let fishPet = pet as Fish;
let birdPet = pet as Bird;
if (fishPet.swim) {
fishPet.swim();
} else if (birdPet.fly) {
birdPet.fly();
}
하지만 이런 스타일의 코드는 원하는 것이 아닐 것 (= in
으로 체크하세요)
사용자 정의 타입 가드
타입 가드 = 어떤 스코프에서 타입을 보장하는 런타임 체크를 실행하는 몇 가지 표현식
타입 예측 사용하기
타입 가드를 정의하기 위해서 간단하게 타입에 대한 예측을 반환하는 함수를 정의하면 됨
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
- 이 예시에서의 타입 예측은
pet is Fish
= 타입 예측의 형태는
parameterName is Type
⚠️ 여기에서
parameterName
은 현재 함수 시그니처에 있는 실제 파라미터 명이어야 함
isFish
가 변수와 함께 호출될 때마다 Typescript는 변수의 범위를 특정 타입으로 좁힘
let pet = getSmallPet();
if (isFish(pet)) { // 이 단락에서 pet은 Fish 타입으로 좁혀짐
pet.swim();
} else {
pet.fly();
}
- Typescript는
if
단락에서pet
이Fish
라는 것을 알 뿐만 아니라,else
단락에서는Fish
가 아니라는 것을 앎 =Bird
isFish
타입 가드를Fish | Bird
배열에서Fish
만 필터링 할 때 사용할 수 있음
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// 또 다른 방법
const underWater2: Fish[] = zoo.filter<Fish>(isFish);
const underWater3: Fish[] = zoo.filter<Fish>((pet) => isFish(pet));
in
연산자 사용하기
in
도 타입을 좁히기 위한 표현식으로 사용될 수 있음
function move(pet: Fish | Bird) {
if ("swim" in pet) {
return pet.swim();
}
return pet.fly();
}
typeof
타입 가드
유니온 타입을 사용하는 padLeft
예제로 돌아가서 다음과 같이 타입 예측을 할 수 있음
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
그렇지만 primitive 타입을 확인하기 위해서 함수를 만드는 것은 번거로움
다행히도 typeof x === "string"
같은 코드를 타입 가드용 함수로 만들지 않아도 됨
→ Typescript가 자체적으로 해당 코드를 타입 가드로 해석함 (아래처럼 인라인으로 체크 가능)
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
여기에서 ===
뒤에 오는 typename은 typeof 연산자가 반환하는 값만 가능하고,
다른 값을 넣는 경우에는 타입 가드로 인식하지 않음
(undefined
, number
, string
, boolean
, bigint
, symbol
, object
, function
)
instanceof
타입 가드
생성자 함수를 이용해서 타입을 좁히는 방법
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5
? new SpaceRepeatingPadder(4)
: new StringPadder(" ");
}
let padder: Padder = getRandomPadder();
// ^ = let padder: Padder
if (padder instanceof SpaceRepeatingPadder) {
padder;
// ^ = let padder: SpaceRepeatingPadder
}
if (padder instanceof StringPadder) {
padder;
// ^ = let padder: StringPadder
}
- 타입이
any
가 아닌 경우 함수의prototype
프로퍼티 타입
- 그 타입의 생성자 Signatures 의해 리턴되는 타입의 Union 타입
이 순서로 타입을 좁혀감
Nullable 타입
- 타입스크립트의 특별한 두 가지 타입 →
null
,undefined
(각각 해당 값을 가짐)(Basic Type 섹션에서 이에 대해 다루었음)
- 기본적으로 타입 체커는
null
과undefined
가 어디에나 할당될 수 있다는 것을 고려함(두 값은 모든 타입에서 valid한 값)
=
null
과undefined
가 어떤 타입에 할당되는 것을 막고 싶을 때 조차도 막을(stop) 수 없음null을 만든 Tony Hoare은 이것을 자신의 "billion dollar mistake"라고 함
--strictNullChecks
플래그로 이 문제를 해결할 수 있음변수를 선언했을 때 이 변수는 자동으로
null
이나undefined
를 가지지 않고가지게 하고 싶다면, union 타입으로 명시적으로 포함시킬 수 있음
let exampleString = "foo";
exampleString = null;
let stringOrNull: string | null = "bar";
stringOrNull = null;
stringOrNull = undefined;
- 두 번째 예제를 통해서 Typescript가
null
과undefined
를 별개로 취급함을 알 수 있음(자바스크립트의 시맨틱에 맞추기 위해서)
string | null
!==string | undefined
!==string | undefined | null
- 타입스크립트 3.7 이상에서는 Optional Chaining으로 Nullable 값을 더 간단하게 처리할 수 있음
Optional 파라미터와 프로퍼티
--stringNullChecks
옵션을 사용하면 옵셔널 파라미터에는 자동적으로 | undefined
가 붙음
function f(x: number, y?: number) {
return x + (y ?? 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null);
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined;
c.b = 13;
c.b = undefined;
c.b = null;
타입 가드와 타입 어설션(assertion)
Nullable 타입은 유니온으로 구현되어 있기 때문에, 타입 가드로 null
을 제거해야 함
다행히 자바스크립트에서 사용하는 코드 그대로 Null 체크가 가능함
function f(stringOrNull: string | null): string {
if (stringOrNull === null) {
return "default";
} else {
return stringOrNull;
}
}
null
제거를 아래와 같이 좀 더 간단하게 표현할 수도 있음
function f(stringOrNull: string | null): string {
return stringOrNull ?? "default";
}
컴파일러가 null
과 undefined
를 자동으로 제거할 수 없는 경우에는 타입 어설션으로 수동 제거 가능
뒤에 !
를 붙여서 identifier!
로 쓰면 null
과 undefined
를 타입에서 제거 가능
interface UserAccount {
id: number;
email?: string;
}
const user = getUser("admin");
user.id;
if (user) {
user.email.length;
}
user!.email!.length;
해당 객체나 필드가 있다는 것을 보장할 수 있다면 !
체인을 통해서 nullability를 쉽게 제거 가능
타입 별칭(Alias)
타입 별칭(type A {}
)은 타입에 새로운 이름을 만들어줌
인터페이스와 어떤 측면에서는 비슷한데,
primitive, union, tuples, 그리고 다른 모든 타입에 이름을 붙일 수 있다는 장점이 있음
type Second = number;
let timeInSecond: number = 10;
let time: Second = 10;
- 실제 타입을 만들어내는 것은 아니고, 그 타입을 가리키는 이름만 만들어내는 것
- primitive 타입에 별칭을 만드는게 기막히게 유용한 방법은 아니지만 Documentation 측면에서 좋음
인터페이스처럼 타입 별칭도 Generic하게 만들 수 있음
type Container<T> = { value: T };
타입 별칭을 타입 내에서 스스로 참조할 때도 사용할 수 있음
type Tree<T> = {
value: T;
left?: Tree<T>;
right?: Tree<T>;
};
intersection 타입과 함께 꽤 맛이 간(mind-bending) 타입을 만들 수 있음 (러시아 인형..)
type LinkedList<Type> = Type & { next: LinkedList<Type> };
interface Person {
name: string;
}
let people = getDriversLicenseQueue();
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;
// ^ = (property) next: LinkedList
인터페이스 vs 타입 별칭
위에서 말한 것처럼 인터페이스와 타입 별칭은 유사한 점이 많지만 또 미묘하게 다름
- interface는 여러 곳에서 사용되는 새로운 이름(new name)을 만듦
- interfaced 위에 마우스를 올리면 에디터는 interfaced가 interface를 return 한다고 보여줌
- 하지만 aliased는 객체 리터럴 타입을 보여줌
- 타입은 새 속성을 추가할 수 없지만 interface는 확장이 가능함

- 이상적인 소프트웨어의 특성은 확장에 열려있기 때문에 (OCP)
가능하다면 타입 별칭보다는 인터페이스 사용을 권함
- 어떤 형태를 인터페이스로 표현할 수 없고 유니온이나 튜플 타입을 사용해야한다면 타입 별칭 쓰기
https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types
Enum 멤버 타입
- Enum 섹션에서 다루었듯이, enum 멤버는 모든 멤버가 초기화 될 때(?) 타입을 가짐
enum members have types when every member is literal-initialized.
다형성(Polymorphic) this
타입
- 포함하고 있는 클래스나 인터페이스의 subtype을 나타냄
= F-바운드 다형성(F-bounded polymorphism) = fluent API 패턴
→ 계층적 인터페이스를 훨씬 쉽게 나타낼 수 있음
class BasicCalculator {
public constructor(protected value: number = 0) {}
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
let v = new BasicCalculator(2).multiply(5).add(1).currentValue();
클래스에서 this
를 사용하기 때문에, 확장할 수 있고 새 클래스는 변경 없이 메서드를 사용할 수 있음
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
let v = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();
this가 없으면 ScientificCalculator
는 BasicCalculator
를 확장할 수 없음
+ fluent 인터페이스를 유지할 수 없음
(fluent 인터페이스 간단 요약 : 메소드 체이닝에 상당 부분 기반한 객체 지향 API 설계 메소드)
Index 타입 - 타입을 동적으로 사용할 때
컴파일러에서 동적 프로퍼티 이름을 사용하는 코드를 확인하도록 할 수 있음
아래 코드는 일반적인 자바스크립트 패턴에서 객체의 프로퍼티를 확인하는 방법
function pluck(o, propertyNames) {
return propertyNames.map((n) => o[n]);
}
타입스크립트에서는..
index type query와 indexed access를 사용할 수 있음 → keyof, T[K]
function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
return propertyNames.map((n) => o[n]);
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: "Toyota",
model: "Camry",
year: 2014,
};
// Manufacturer와 model은 둘 다 string 타입
// 따라서 둘 다 string 배열을 만들기 위해서 pluck(= 뽑다) 가능함
let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
// model과 year 속성을 pluck 하면
// union type 배열을 얻게 됨 (string | number)[]
let modelYear = pluck(taxi, ["model", "year"]);
컴파일러는 manufacturer
와 model
이 Car
의 프로퍼티인지를 확인
keyof
: 인덱스 타입 쿼리 연산자, Props의 타입에 존재하는 type들을 union 타입으로 할당
let carProps: keyof Car;
// ^ = let carProps: "manufacturer" | "model" | "year"
공식문서랑 주석을 다르게 썼는데, 본문 읽어보니까 이상한듯해서 바꿈요..
- 여기에서
keyof Car
="manufacturer" | "model" | "year"
둘이 완전 똑같음
- 다른 점은 Car에 또 다른 프로퍼티를 추가하면 (예를 들어:
ownersAddress: string
)keyof Car
는 자동적으로"manufacturer" | "model" | "year" | "ownersAddress"
로 업데이트 됨
keyof
를 위의 예제처럼 제너릭과 사용할 수도 있음 (당시에 프로퍼티 이름을 모를 때)
pluck(taxi, ["year", "unknown"]);
T[K]
: indexed access operator, 인덱싱된 접근 연산자
- 타입 syntax는 expression syntax를 반영함
=
taxi["manufacturer"]
는Car["manufacturer"]
타입을 가짐
- 인덱스 타입 질의와 마찬가지로 제너릭을 사용할 수 있음
타입 변수에 제너릭 변수
K extends keyof T
를 확실하게 명시해야 함
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName] is of type T[K]
}
위 예제의 o: T
와 propertyName: K
에서 o[propertyName]: T[K]
를 의미함 (왓더..)
let manufacturer: string = getProperty(taxi, "manufacturer");
let year: number = getProperty(taxi, "year");
let unknown = getProperty(taxi, "unknown");
getProperty
의 반환 결과물은 어떤 파라미터를 넘기느냐에 따라 달라짐
Index 타입과 Index 시그니처
keyof
와 T[K]
는 인덱스 시그니처와 상호작용함
- 인덱스 시그니처 파라미터의 타입은 반드시
string
또는number
여야 함string
타입의 인덱스 시그니처를 넘기면,keyof T
는string | number
가 됨= 그냥
string
이 아님 (왜냐면 이렇게도 접근이 가능해서 →Object["42"]
,Object[42]
)
interface Dictionary<T> {
[key: string]: T;
}
let keys: keyof Dictionary<number>;
// ^ = let keys: string | number
let value: Dictionary<number>["foo"];
// ^ = let value: number
인덱스 시그니처 타입이 number
이면 string | number
아니고 그냥 number
됨
interface Dictionary<T> {
[key: number]: T;
}
let keys: keyof Dictionary<number>;
// ^ = let keys: number
let numberValue: Dictionary<number>[42];
// ^ = let numberValue: number
let value: Dictionary<number>["foo"];
매핑된(Mapped) 타입
기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법
(마치 자바스크립트 map()
API 함수를 타입에 적용한 것과 같은 효과)
일반적인 작업은 기존 타입을 가져와서 각 프로퍼티를 옵셔널하게 만드는 것
interface PersonSubset {
name?: string;
age?: number;
}
또는 readonly로 만드는 것
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
바로 이렇게
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type PersonPartial = Partial<Person>;
// ^ = type PersonPartial = {
// name?: string | undefined;
// age?: number | undefined;
// }
type ReadonlyPerson = Readonly<Person>;
// ^ = type ReadonlyPerson = {
// readonly name: string;
// readonly age: number;
// }
주목할 부분은 멤버에 대해서가 아니라 타입에 대해서 묘사하고 있다는 것
→ 기존 타입을 기반으로 새로운 멤버를 추가하고 싶은 것이라면 intersection 타입을 사용해야 함
// 이렇게 쓰기 (거의 뭐 암호 수준인데;)
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
} & { newMember: boolean }
// 에러 납니다
type WrongPartialWithNewMember<T> = {
[P in keyof T]?: T[P];
newMember: boolean;
}
간단한 예제로 다시 살펴봅시다
type Keys = "option1" | "option2";
type Flags = { [K in Keys]: boolean };
내부에 for ... in
이 있는 인덱스 시그니처 구문과 유사함
- 타입 변수
K
는 차례대로 각 프로퍼티에 바인딩됩니다.
- 반복 처리할 프로퍼티의 이름이 들어있는 문자열 리터럴 Union Keys
- 프로퍼티의 결과 타입 = boolean
이 결과는 다음과 같음
type Flags = {
option1: boolean;
option2: boolean;
};
현실 세계의 예제에서는 이렇게 하드코딩(Keys)된 케이스보다 이미 존재하는 타입을 쓰는 경우가 더 많음
위에서 정리했던 keyof
와 indexed access types를 활용할 때..
type NullablePerson = { [P in keyof Person]: Person[P] | null };
// ^ = type NullablePerson = {
// name: string | null;
// age: number | null;
// }
type PartialPerson = { [P in keyof Person]?: Person[P] };
// ^ = type PartialPerson = {
// name?: string | undefined;
// age?: number | undefined;
// }
제너릭한 버전
type Nullable<T> = { [P in keyof T]: T[P] | null };
type Partial<T> = { [P in keyof T]?: T[P] };
- 프로퍼티 목록 :
keyof T
- 결과로 나오는 타입 :
T[P]
의 변형
매핑된 타입으로부터의 인터페이스 - unwrapping
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);
// ^ = let originalProps: {
// rooms: number;
// }