Javascript에 존재하는 기본 타입들은 타입을 정의하는 게 기반이 되지만 모든 복잡한 상황에 대해서는 제대로 대응하지 못함. Typescript는 그러한 상황에 맞는 다양한 타입들을 추가적으로 제공하고 있음.
1. 유니언 타입(Union Type)
둘 이상의 타입에 대해 그 중 하나를 타입으로 가질 수 있는 새로운 타입을 정의하는 방법으로, OR 기호 "|"를 이용해 타입을 조합해줄 수 있음. 조합에 사용된 각각의 타입을 유니언 타입의멤버라고 부름. 예를 들어, 문자열 또는 숫자를 argument로 받을 수 있는 함수는 다음과 같이 타입을 정의해줄 수 있음.
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
한편, 함수 내에서 특정한 타입에서만 존재하는 메서드나 속성을 사용할 경우 오류가 발생할 수 있음. 예를 들어, number 또는 string인 유니언 타입 argument에 대해 string에만 존재하는 toUpperCase() 메서드를 사용할 경우Property 'toUpperCase' does not exist on type 'number'과 같은 오류 메시지를 보게 됨. 유니언 타입의 값에 특정 메서드, 속성을 사용할 때,모든 멤버에 존재하는 메서드, 속성이 아니라면 경고가 뜨므로 주의!
function printId(id: number | string) {
console.log(id.toUpperCase());
}
해결법은 각각의 타입에 대해 if를 적용해 타입에 따른 메서드를 적용하는 방법이 있음. 하지만, Javascript에서도 함수의 매개변수로 여러 타입을 받을 때는 타입에 따라 메서드, 속성 사용을 달리 해야 하기 때문에 Typescript에서만 특별히 신경써야 할 부분이 아님!
A. never 타입과 유니언 타입
type string = string | never; // string
never 타입은 공집합과 같으므로 다른 타입들과 합쳐졌을 때 사라짐. 이러한 특징은 타입 맵핑으로 특정한 조건을 만족하는 타입의 키를 제거할 때 유용하게 쓰임.
B. 예시
1. 유니언 타입에 들어있는 타입인지 여부 확인하기
type Color = "red" | "blue" | "green";
특정한 변수가 Color 유니언 타입에 존재하는 타입인지 확인하려면 어떻게 해야 할까? 유니언 타입과 인터섹션 타입을 같이 사용해주면 됨. Color 타입에 존재하지 않는 타입과 인터섹션 타입을 이루면 그 결과는 never 타입. 이 자체로는 유용한 타입이 아니지만 제네릭 타입으로 사용한다면 유용하게 쓰일 수 있음.
type SelectedColor<T> = T & Color;
C. 주의할 점
1. Function 타입과 유니언 타입을 구성할 때
type Mixed = null | ((id: string) => void);
Function 타입과 유니언 타입을 구성할 때는 Function 타입에 ( () => void )처럼 괄호를 넣어줘야 함에 유의!
2. 인터섹션 타입(Intersection Type)
유니언 타입이 OR의 개념이라면, 인터섹션 타입은 AND의 개념. 둘 이상의 타입을 & 연산자로 조합해 해당 타입을 모두 만족하는 새로운 타입을 만들 때 사용함.
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
ColorfulCircle은 Colorful, Circle 타입의 속성을 모두 가지고, 해당 속성이 원래의 타입을 가지는 새로운 타입.
A. 기본 타입과 인터섹션 타입
type neverType = string & number; // never
type string = string & unknown; // string
인터섹션 타입은 그 명칭이 의미하듯 두 타입 간의 교집합인 타입을 의미함. string, number 타입에는 교집합이 없으므로string & number는 never와 같음. 또한, unknown, any 타입은 전체 집합인 셈이므로 string & unknown는 string이 됨.
B. never 타입과 인터섹션 타입
type neverType = string & never; // never
never 타입은 공집합과 같으므로 다른 타입들과 인터섹션을 하면 그 결과도 never 타입이 됨.
3. 타입 별칭(Type Alias)
A에서 number 또는 string 타입을 가지는 변수가 빈번하게 쓰일 경우 모든 코드에 "number | string"을 써주는 건 무척 비효율적. 이때,type키워드로 타입에 이름을 붙여주면 해당 유니언 타입을 재사용할 수 있음.
type ID = number | string;
타입 별칭은 유니언 타입에 붙여준 이름일 뿐, 다른 동일한 형태의 타입과 구분을 지어주지는 않음. 즉, Typescript는타입 별칭으로 타입 검사를 하는 것이 아니라타입 별칭의 각 멤버에 대해 타입 검사를 함. 예를 들어, getUserInput() 함수의 반환값은 유니언 타입ID의 타입을 가짐. 하지만, getUserInput()의 반환값은 getString(str)로ID타입이 아니라string타입이라서오류가 발생할 거라 생각할 수 있지만 아님. ID 타입과 string 타입은 이름이 다르지만,ID 타입에 string 타입이 포함되어 있기 때문에 문제가 없음.
type ID = number | string;
function getString(str: string): string {
return str.toLowerCase();
}
function getUserInput(str: string): ID {
return getString(str);
}
위 함수처럼 특정한 문자열을 받아 작업을 수행하는 함수를 생각해볼 수 있음. operation에 string 타입을 지정해주기만 해도 default를 제대로 설정했다면 문제가 없지만, 제한된 작업만 하는 함수에 제한적인 타입을 지정할 필요성이 생김.구체적인 문자열, 숫자 값을 지정해줄 수 있는 리터럴 타입 자체로는 큰 의미가 없지만,유니언 타입과 함께 사용하면 보다 유연한 타입 정의가 가능해짐!
type operation = "ADD" | "DELETE" | "MODIFY";
B. 템플릿 리터럴 타입(Template Literal Types)
Javascript 문법인 템플릿 리터럴은 타입 정의할 때도 사용할 수 있는데, 특정 문자열만 가능한 타입을 이어붙여 새로운 타입을 만드는 데 쓰일 수 있음.
type data = "task" | "tag"
type operationT = "ADD" | "MODIFY" | "DELETE";
type action = `${data}/${operationT}`
예를 들어, 어떠한 문자열의 / 왼쪽에는 "task"나 "tag", 오른쪽에는 "ADD", "MODIFY", "DELETE" 중 하나가 올 수 있는 상황을 가정해보자. "task/ADD", ... 하나하나 타입을 만드는 것보다 템플릿 리터럴을 사용하면 간편한 타입 정의가 가능함. 이렇게 만들어진 action 타입은 아래와 같은 문자열들만 허용함.
Optional Property와 유사하지만 불확실한 속성이 수 십개가 된다면 Optional Property를 사용할 수는 없음. 이런 경우처럼 Index Signature는속성명은 모르지만속성과 속성값이 어떠한 타입일지는 아는 상황일 때 쓸 수 있는데, "[x:타입]: 타입"와 같이 사용함. 이때, Index Signature는숫자또는문자열만 가능함.
A. No index signature with a parameter of type 'string' was found
function ObjectMatcher(str: string) {
const obj = {
prop1: 1,
prop2: 2,
};
// ❌ No index signature with a parameter of type 'string' was found
return obj[str];
}
위와 같이 문자열을 인수로 받아 특정 객체의 value 값을 반환하는 함수를 작성했을 때, '{ prop1: number; prop2: number; }' 형식에서 'string' 형식의 매개 변수가 포함된 인덱스 시그니처를 찾을 수 없습니다.라는 에러가 나타남. 객체 obj의 키가 string 타입인 건 맞지만 임의의 문자열은 아니기 때문에 이러한 에러가 발생함. string 타입을 'prop1' | 'prop2' 타입에 매칭할 수 없기 때문.
따라서, 임의의 문자열인 string 타입을 keyof obj 로 좁혀줄 필요가 있음. Javascript의 in 연산자를 이용할 수 있지만, Typescript 컴파일러는 문자열 변수에 대해서는 in 연산자로 타입 좁히기를 적용하지 않기 때문에 Type Predicate를 별도로 만들어줘야 함.
// 문자열 s가 객체 obj의 키일 때 true를 반환하는 Type Predicate
function isKeyOf<T>(s: PropertyKey, obj: T): s is keyof T {
return s in obj;
}
Javascript에서 객체 obj에 obj["prop1"]을 사용해 prop1 속성에 접근할 수 있듯이, 타입이 객체 형태일 때도 동일한 방식으로 해당 속성의 타입에 접근할 수 있음. 아래 예시에서 Age 타입은 Person의 age 속성이 가지는 타입인 number을 가짐.
interface Person {
age: number,
name: string,
alive: boolean
};
type Age = Person["age"];
한편, Indexing Type도 타입이므로 위 예시처럼 하나의 타입 "age"만 올 수 있지 않고, "age" | "alive"처럼 다른 타입과 함께 쓰일 수 있음.
type I1 = Person["age" | "name"]; // string | number
type I2 = Person[keyof Person]; // string | number | boolean
type otherType = {
prop1: string,
prop2: number,
prop3: string[]
}["prop1" | "prop2"]
otherType은 { ... }["prop1"] | { ... }["prop2"]인 유니언 타입으로, 그 결과는 string | number가 됨.
A. 인터페이스의 각 속성의 타입만을 가지는 유니언 타입 만들기
위와 같은 인터페이스가 존재할 때, 각 속성의 타입만을 골라 아래와 같은 ServerSortOptions 타입을 만들려고 함.
interface SortOptions {
거리순: "distance";
최신순: "createdAt";
리뷰수순: "reviewCount";
별점순: "score";
}
// ❓ 이런 타입을 만들고 싶은데...
type ServerSortOptions = "distance" | "createdAt" | "reviewCount" | "score";
인덱스 접근 타입(Indexed Access Type)을 이용하면 각 속성의 타입을 모은 유니언 타입을 만들어줄 수 있음.
Typescript의 다양한 타입들
Javascript에 존재하는 기본 타입들은 타입을 정의하는 게 기반이 되지만 모든 복잡한 상황에 대해서는 제대로 대응하지 못함. Typescript는 그러한 상황에 맞는 다양한 타입들을 추가적으로 제공하고 있음.
1. 유니언 타입(Union Type)
둘 이상의 타입에 대해 그 중 하나를 타입으로 가질 수 있는 새로운 타입을 정의하는 방법으로, OR 기호 "|"를 이용해 타입을 조합해줄 수 있음. 조합에 사용된 각각의 타입을 유니언 타입의 멤버라고 부름. 예를 들어, 문자열 또는 숫자를 argument로 받을 수 있는 함수는 다음과 같이 타입을 정의해줄 수 있음.
한편, 함수 내에서 특정한 타입에서만 존재하는 메서드나 속성을 사용할 경우 오류가 발생할 수 있음. 예를 들어, number 또는 string인 유니언 타입 argument에 대해 string에만 존재하는 toUpperCase() 메서드를 사용할 경우 Property 'toUpperCase' does not exist on type 'number'과 같은 오류 메시지를 보게 됨. 유니언 타입의 값에 특정 메서드, 속성을 사용할 때, 모든 멤버에 존재하는 메서드, 속성이 아니라면 경고가 뜨므로 주의!
해결법은 각각의 타입에 대해 if를 적용해 타입에 따른 메서드를 적용하는 방법이 있음. 하지만, Javascript에서도 함수의 매개변수로 여러 타입을 받을 때는 타입에 따라 메서드, 속성 사용을 달리 해야 하기 때문에 Typescript에서만 특별히 신경써야 할 부분이 아님!
A. never 타입과 유니언 타입
never 타입은 공집합과 같으므로 다른 타입들과 합쳐졌을 때 사라짐. 이러한 특징은 타입 맵핑으로 특정한 조건을 만족하는 타입의 키를 제거할 때 유용하게 쓰임.
B. 예시
1. 유니언 타입에 들어있는 타입인지 여부 확인하기
특정한 변수가 Color 유니언 타입에 존재하는 타입인지 확인하려면 어떻게 해야 할까? 유니언 타입과 인터섹션 타입을 같이 사용해주면 됨. Color 타입에 존재하지 않는 타입과 인터섹션 타입을 이루면 그 결과는 never 타입. 이 자체로는 유용한 타입이 아니지만 제네릭 타입으로 사용한다면 유용하게 쓰일 수 있음.
C. 주의할 점
1. Function 타입과 유니언 타입을 구성할 때
Function 타입과 유니언 타입을 구성할 때는 Function 타입에 ( () => void )처럼 괄호를 넣어줘야 함에 유의!
2. 인터섹션 타입(Intersection Type)
유니언 타입이 OR의 개념이라면, 인터섹션 타입은 AND의 개념. 둘 이상의 타입을 & 연산자로 조합해 해당 타입을 모두 만족하는 새로운 타입을 만들 때 사용함.
ColorfulCircle은 Colorful, Circle 타입의 속성을 모두 가지고, 해당 속성이 원래의 타입을 가지는 새로운 타입.
A. 기본 타입과 인터섹션 타입
인터섹션 타입은 그 명칭이 의미하듯 두 타입 간의 교집합인 타입을 의미함. string, number 타입에는 교집합이 없으므로 string & number는 never와 같음. 또한, unknown, any 타입은 전체 집합인 셈이므로 string & unknown는 string이 됨.
B. never 타입과 인터섹션 타입
never 타입은 공집합과 같으므로 다른 타입들과 인터섹션을 하면 그 결과도 never 타입이 됨.
3. 타입 별칭(Type Alias)
A에서 number 또는 string 타입을 가지는 변수가 빈번하게 쓰일 경우 모든 코드에 "number | string"을 써주는 건 무척 비효율적. 이때, type 키워드로 타입에 이름을 붙여주면 해당 유니언 타입을 재사용할 수 있음.
타입 별칭은 유니언 타입에 붙여준 이름일 뿐, 다른 동일한 형태의 타입과 구분을 지어주지는 않음. 즉, Typescript는
타입 별칭으로 타입 검사를 하는 것이 아니라타입 별칭의 각 멤버에 대해 타입 검사를 함. 예를 들어, getUserInput() 함수의 반환값은 유니언 타입 ID의 타입을 가짐. 하지만, getUserInput()의 반환값은 getString(str)로 ID 타입이 아니라 string 타입이라서 오류가 발생할 거라 생각할 수 있지만 아님. ID 타입과 string 타입은 이름이 다르지만, ID 타입에 string 타입이 포함되어 있기 때문에 문제가 없음.4. 리터럴 타입(Literal Types)
A. 리터럴 타입(Literal Types)
위 함수처럼 특정한 문자열을 받아 작업을 수행하는 함수를 생각해볼 수 있음. operation에 string 타입을 지정해주기만 해도 default를 제대로 설정했다면 문제가 없지만, 제한된 작업만 하는 함수에 제한적인 타입을 지정할 필요성이 생김. 구체적인 문자열, 숫자 값을 지정해줄 수 있는 리터럴 타입 자체로는 큰 의미가 없지만, 유니언 타입과 함께 사용하면 보다 유연한 타입 정의가 가능해짐!
B. 템플릿 리터럴 타입(Template Literal Types)
Javascript 문법인 템플릿 리터럴은 타입 정의할 때도 사용할 수 있는데, 특정 문자열만 가능한 타입을 이어붙여 새로운 타입을 만드는 데 쓰일 수 있음.
예를 들어, 어떠한 문자열의 / 왼쪽에는 "task"나 "tag", 오른쪽에는 "ADD", "MODIFY", "DELETE" 중 하나가 올 수 있는 상황을 가정해보자. "task/ADD", ... 하나하나 타입을 만드는 것보다 템플릿 리터럴을 사용하면 간편한 타입 정의가 가능함. 이렇게 만들어진 action 타입은 아래와 같은 문자열들만 허용함.
5. 인덱스 서명(Index Signature) #
Optional Property와 유사하지만 불확실한 속성이 수 십개가 된다면 Optional Property를 사용할 수는 없음. 이런 경우처럼 Index Signature는 속성명은 모르지만 속성과 속성값이 어떠한 타입일지는 아는 상황일 때 쓸 수 있는데, "[x: 타입]: 타입"와 같이 사용함. 이때, Index Signature는 숫자 또는 문자열만 가능함.
A. No index signature with a parameter of type 'string' was found
위와 같이 문자열을 인수로 받아 특정 객체의 value 값을 반환하는 함수를 작성했을 때, '{ prop1: number; prop2: number; }' 형식에서 'string' 형식의 매개 변수가 포함된 인덱스 시그니처를 찾을 수 없습니다. 라는 에러가 나타남. 객체 obj의 키가 string 타입인 건 맞지만 임의의 문자열은 아니기 때문에 이러한 에러가 발생함. string 타입을 'prop1' | 'prop2' 타입에 매칭할 수 없기 때문.
따라서, 임의의 문자열인 string 타입을 keyof obj 로 좁혀줄 필요가 있음. Javascript의 in 연산자를 이용할 수 있지만, Typescript 컴파일러는 문자열 변수에 대해서는 in 연산자로 타입 좁히기를 적용하지 않기 때문에 Type Predicate를 별도로 만들어줘야 함.
6. 인덱스 접근 타입(Indexed Access Type) #
Javascript에서 객체 obj에 obj["prop1"]을 사용해 prop1 속성에 접근할 수 있듯이, 타입이 객체 형태일 때도 동일한 방식으로 해당 속성의 타입에 접근할 수 있음. 아래 예시에서 Age 타입은 Person의 age 속성이 가지는 타입인 number을 가짐.
한편, Indexing Type도 타입이므로 위 예시처럼 하나의 타입 "age"만 올 수 있지 않고, "age" | "alive"처럼 다른 타입과 함께 쓰일 수 있음.
otherType은 { ... }["prop1"] | { ... }["prop2"]인 유니언 타입으로, 그 결과는 string | number가 됨.
A. 인터페이스의 각 속성의 타입만을 가지는 유니언 타입 만들기
위와 같은 인터페이스가 존재할 때, 각 속성의 타입만을 골라 아래와 같은 ServerSortOptions 타입을 만들려고 함.
인덱스 접근 타입(Indexed Access Type)을 이용하면 각 속성의 타입을 모은 유니언 타입을 만들어줄 수 있음.
7. unknown 타입
https://dmitripavlutin.com/typescript-unknown-vs-any/
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type
'TypeScript' 카테고리의 다른 글