TypeScript
Part.2
chapter.9 타입 제한자
9.1 top 타입
bottom 타입 개념과 반대되는 top타입
bottom 타입 되돌아보기
- 가능한 값이 없고, 접근이 불가능한 타입을 나타내기 위한
never
타입을bottom
타입이라고도 부른다.
1
2
3
4
5
6
7
8
9
10
11
// bottom 타입의 예제
function throwError(): never{
throw new Error("Error")
} // 함수가 예외를 던지는 경우 : 반환값이 없다.
function infiniteLoop(): never{
while(true){
...
}
} // 무한 루프를 발생시키는 경우 : 반환값이 없다.
void
도 반환값이 없지 않나?
never
와void
의 차이,never
타입은 함수가 항상 예외를 던지거나 무한 루프에 빠져서 반환값이 없는 경우이고void
는 함수가 단순히 값을 반환하지 않는 타입이다.1 2 3 4 5 6 7
function exampleVoid(num: number): void { console.log(num); } // return 값이 없는 void function exampleNever(): never { throw new Error("error"); } // 반환할 수 없는 함수인 never
그래서 bottom 타입과 다른 top 타입이라는 건 뭐야?
- 모든 값의 타입에 알 수 없는
unknwon
타입을 말한다. unknown
타입은 모든 값의 타입에 대해 알 수 없는 상태를 나타내는 타입이며any
타입과 유사하지만unknown
타입이 더 안전한 타입 체크를 제공한다.
any
타입 되돌아보기
any
타입은 어떠한 타입도 올 수 있기 때문에 타입 검사를 수행하지 않는 것과 같다.따라서 타입스크립트에서 오류가 발생하지 않더라도 런타임 환경에서는 오류가 발생할 가능성이 있다.
1 2 3 4 5 6 7 8 9 10 11
function objectKey(obj: any, key: string) { return obj[key]; } // 입력받은 객체의 key값 알아내기 let obj = { name: "choi", age: 28 }; let getName = objectKey(obj, "name"); // OK // 입력받은 obj는 any타입이기 때문에 객체가 아니어도 오류가 아니다. obj = "Error"; let getError = objectKey(obj, "name"); // OK // 객체가 아니기 때문에 'name' 값을 얻을 수 없고, 런타임에서 오류가 발생한다.
unknown
이 더 안전한 이유는 뭘까
타입스크립트에서
unknown
을 사용할 경우, 추가적인 타입 체크를 요구하기 때문에 반드시 타입 체크를 수행해야 한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
function exampleUnknownError(value: unknown) { return value.toString(); // Error: 'value' is of type 'unknown'. // value는 unknown 타입이기 때문에 // 추가적인 타입 체크가 필요하다. } function exampleUnknownCheck(value: unknown) { // Error: not all code path return a value if (typeof value === "number") { return value.toString(); } // 타입체크는 했지만 number 타입이 아닐 때에 // return이 없다. } function exampleUnknown(value: unknown) { if (typeof value === "number") { return value.toString(); } return ""; }
9.2 타입 서술어
타입 서술어가 필요한 이유
1
2
3
4
5
6
7
8
9
10
11
12
13
function numberOrString(value: unknown) {
return ["number", "string"].includes(typeof value);
}
function useFunction(value: number | string | null | undefined) {
if (numberOrString(value)) {
value.toString();
// numberOrString이 true일 때만 toString()
// 을 실행하기 때문에 문제가 없지 않을까?
} else {
console.log("value does not exist:", value);
}
}
- 우리의 의도는 함수
numberOfString
을 사용하여 true일 때만toString()
이지만, 타입스크립트는 위의 함수를 참고하여 number,string일 때만toString()
을 사용한다고 생각하지 않는다. - 그저 value값의 가능성만 확인하여 위의 함수가 true이든 false이든 상관없이 어쨌든 null과 undefined는
toString()
을 사용할 수 없다고 판단하기 때문에 오류가 발생한다.
is 사용하여 타입 좁히기
- 타입스크립트에서 어떠한 타입을 가져야 한다는 것을
is
를 통해 나타낼 수 있다. - 타입 가드 함수란, 타입스크립트에서 타입을 정확하게 추론하기 위한 방법이며
boolean
타입을 갖는다.- 단순히
boolean
을 갖는 것이 아니라 더 구체적인 타입임을 나타낸다.
- 단순히
1
2
3
4
5
6
7
8
9
function exampleIs(value: unknown): value is number | string {
return ["number", "string"].includes(typeof value);
}
function exampleIsError(value: unknown): value is number | string {
return value;
// Error: Type 'unknown' is not assignable to type 'boolean'
// 타입 가드는 boolean 타입을 나타내야 한다.
}
인터페이스 검사하기
인터페이스로 알려진 객체가 더 구체적인 인터페이스의 인스턴스인지 여부를 검사한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
interface Comedian{ funny: boolean; } interface StandupComedian extends Comedian{ routine: string; } function isStandupComedian(value: Comedian): value is StandupComedian{ return 'routine' in value; } function workWithComedian(value: Comedian){ is(isStandupComedian(value)){ console.log(value.routine) // OK } console.log(value.routine); // Error: Property 'routine' does not exist on type 'Comedian'. }
- 인터페이스
Comedian
은 routine을 갖고 있을 수도 없을 수도 있는데, routine이 있다면StandupComedian
으로 간주가 된다.
- 인터페이스
9.3 타입 연산자
9.3.1 keyof
- 자바스크립트에서는 일반적으로
string
타입인key
를 사용하여 객체의 값을 찾지만, 타입스크립트의string
타입은 포괄적인 의미를 갖기 때문에 유효하지 않은 키가 허용될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
interface Ratings {
audience: number;
critics: number;
}
function getRating(ratings: Ratings, key: string): number {
return ratings[key];
// Error: Element implicitly has an 'any' type because expression
// of type 'string' Can't be used to index type 'Ratings'
// No index signature with a parameter of type 'string' was found on type
// 'Ratings'.
}
getRating(ratings, 'errorExample')
처럼 ratings에 들어있지 않는 값에 대해 허용될 수 있기 떄문에 오류가 발생한다.
해결 옵션
리터럴 유니언 타입을 사용하여, 존재하는 키로 제한한다.
1 2 3
function getRating(ratings: Ratings, key: "audience" | "critic"): number { return ratings[key]; }
keyof
연산자를 사용하여 모든 키의 조합을 사전에 정의한다.1 2 3
function getRating(ratings: Ratings, key: keyof Ratings): number { return ratings[key]; }
9.3.2 typeof
typeof
연산자는 제공되는 값의 타입을 반환한다.1 2 3 4 5 6 7 8 9 10 11
const original = { medium: "movie", // string title: "Mean Girls", // string }; let example = typeof original; let sameExample: { medium: string; title: string; };
변수로 선언된 객체와 인터페이스 객체는 무슨 차이일까?
1 2 3 4 5 6 7 8 9 10 11 12
interface Literal { key1: "value"; key2: "value"; } let literal = { key1: "value", key2: "value", }; let example1: typeof literal; let example2: Literal;
- 변수
literal
은 객체 리터럴로 생성된 값이며, 인터페이스Literal
은 객체의 타입을 정의한다. - 똑같은
key
와value
값을 갖고 있지만 인터페이스는 문자열value
로 고정되어 있어, 이 외의 문자열은 할당할 수 없다.
- 변수
keyof
타입 연산자와typeof
타입 연산자를 함께 사용해서, 값의 타입에 허용된 키를 간결하게 검색할 수 있다.1 2 3 4 5 6 7
const example = { key: "examle", }; function exampleFun(key: keyof typeof example) { return example[key]; }
- 오직
example
에 있는key
만 찾는다.
- 오직
9.4 타입 어서션
- 타입스크립트 안에서 타입을
미리 가정
하는 것이다. - 타입 어서션은 해당 문이 언제나
참
이라고 간주하기 때문에, 타입스크립트에서는 에러가 발생하지 않아도 자바스크립트에서는 에러가 발생할 수 있다. - 무슨 말이냐
1 2 3 4 5 6
const num: number = 1; // number으로 선언된 num console.log((num as unknown as Array<number>).push(1)); // 의도적으로 number를 가진 배열이라고 알려준다. // num은 number이기 때문에 push(1)을 사용할 수 없지만 // 타입스크립트는 참으로 인식한다.
타입 어서션은 언제 사용하나
- 위 예시처럼 타입 어서션을 쓰는 것을 피하는 것이 좋으나
JSON.parse
처럼, 타입스크립트에서 의도적으로any
타입을 반환하는 경우 특정 타입을 알려주기 위해서 사용하거나 타입에 대한 확신이 있을 때만 사용하는 것이 좋다.
타입 어서션은 어떻게 사용하나
타입 어서션은 두 가지 문법으로 표현된다.
1 2 3 4 5 6 7
// 1. 앵글 브라킷을 사용한다. const exampleAssertion: any = "hello"; const exampleNumToStr: number = (<string>exampleAssertion).length; // 2. as 를 사용한다. const exampleAssertion: any = "hello"; const exmapleNumToStr: number = (exampleAssertion as string).length;
- 앵글 브라킷은 React의 JSX문법과 같다고 인식되기 때문에 되도록 as를 쓰는 것이 좋다.
9.4.1 포착된 오류 타입 어서션
- API를 사용할 때 반환값의 타입을 알고 있지만 해당 타입을 타입스크립트가 추론하지 못하는 경우가 있을 수 있다.
- 이때 타입 어서션을 사용하여 해당 타입을 좁혀 처리할 수 있다.
- 외부 라이브러리나 모듈을 사용할 때 해당 모듈의 타입을 인식하지 못하는 경우가 있는데, 해당 모듈에 대한 타입도 정의할 수 있다.
try...catch
문에서 발생한 Error 객체에는 다양한 속성과 메서드를 가지고 있는데, 이 중message
가 가지고 있는 속성의 정보에 접근할 수 있게 한다.- 어떤 타입을 가지고 있는지 명시적으로 지정하여 코드 안정성을 높여준다.
굳이 왜 error에 어서션을 사용해야 할까
- Error 객체는
unknown
이다. 우리가 Error에서 뭔가를 꺼내오고 싶거나, 에러가 어디서 발생했는지 확인하기 위해 Error의 내용을 확인해야 되는 경우가 생기는데, 이때 Error는 unknown이기 때문에 어서션을 사용해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
try { throw new Error("Error"); } catch (error) { (error as Error).message; } // 에러가 메세지를 안 갖고 있는 경우, 에러를 찍었는데 그게 또 에러일 수 있다. // 또한 에러가 어떤 에러인지 추론할 수 없기 때문에 조건문을 사용하면 좋다. try { throw "Error"; } catch (error) { (error as Error).message; // "Error"자체가 Error이기 때문에 message를 찍는 것 자체가 에러이다. } // 조건문을 활용한 예시 try { axios.put("https://naver.com"); } catch (error) { if (error instanceof AxiosError) { console.error("Axios Error >>", error.code); } if (error instanceof Error) { console.error("Error >>", error.message); } }
9.4.2 non-null 어서션
- 단언 연산자라고도 부르며, 피연산자가
null
,undefined
라고 아니라고 단언해준다.
non-null 어서션 필요 상황
- 우리가 사용하고자 하는 요소나 값을 타입스크립트가 추론하지 못 하기 때문에
null
을 반환을 한다. - non-null이 없다고 단언하기 때문에 타입스크립트의 안쩡성을 해칠 수 있어 권장되진 않는다.
1
2
3
4
5
6
7
8
9
10
11
const button = document.querySelector("button");
// const button: HTMLButtonElement | null
// 이런 상황에서 버튼이 있는지 확인을 해줘야 한다.
if (button) button;
// 이런 상황에서 버튼이 있는지 확인하지 않고
// non-null 어서션을 사용하면
//button! 있다고 가정되어 바로 사용이 가능하다.
button!.textContent = "";
// 만약 어서션을 사용하지 않을 시
button.COMMENT_NODE;
// ERROR : 'button' is possibly 'null'.
9.4.3 타입 어서션 주의 사항
any
타입을 사용할 때처럼 꼭 필요한 경우가 아니라면 가능한 사용하지 말아야 한다.타입스크립트에서는 아무런 오류가 없지만 런타임 오류가 발생할 수 있다.
1 2 3 4 5 6 7 8 9 10
const seasonCounts = new Map([ ["Broad City", 5], ["Community", 6], ]); const knownValue = seasonCounts.get("I Love City")!; // non-null 어서션 사용 // undefined 값을 임의로 없다고 가정 console.log(knownValue.toUpperCase()); // Runtimes TypeError: Cannot read property 'toUpperCase' of undefined
어서션 vs 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Entertainer {
acts: string[];
name: string;
}
const exampleRight: Entertainer = {
name: "need acts",
// Error: Property 'acts' is missing in type
// 인터페이스 Entertainer를 참조하기 때문에 acts 프로퍼티가 필요하다.
};
const exampleWrong = {
name: "need acts",
} as Entertainer;
// 인터페이스 Entertainer를 따른다고 명시적으로 나타내는 것 자체에 문제가 없다.
// exampleWrong이 인터페이스를 따르는 속성과 값을 가진다고 가정한다.
어서션 할당 가능성
- 타입 중 하나가 다른 타입에 할당 가능한 경우에만 타입 어서션이 허용된다.
완전히 서로 관련이 없는 타입 사이에는 타입 오류를 감지한다.
1 2 3 4
let assertion = "Example" as number; const exampleWrong = 1 as Entertainer; // Error const exampleWrong = 1 as unknown as Entertainer; // OK
9.5 const 어서션
const
어서션은 모든 값을 상수로 취급해야 함을 나타낼 때 사용한다.- 배열은 가변 배열이 아니라 읽기 전용으로 취급한다.
- 리터럴은 일반적은 원시 타입(string,number)와 동일하지 않고 리터럴로 취급한다.
- 객체의 속성은 읽기 전용으로 간주된다.
코드의 가독성과 유지보수성을 높이고, 에기치 않은 값 변경으로 인한 오류를 방지하기 위해 사용한다.
1 2 3 4 5
const myNumber: number = 0; // myNumber의 값은 숫자인 0으로 고정된다. let exampleNum = 10 as const; exampleNum = 0; // Error: Type '0' is not assignable to type '10'.
9.5.1 리터럴에서 원시 타입으로
- 함수에 대해 특정 리터럴이 오도록 타입스크립트에게 알려줄 수도 있고, 구체적인 값을 지정할 수도 있다.
왜 하냐, 예기치 않은 값이 올 때를 방지하여 안정성을 높이기 위해 사용한다.
1 2 3 4
const getName = () => "Maria Bamford"; // "Maria Bamford"를 리턴하는 함수 getName에게 const getName = () => "Maria Bamford" as const; // "Maria Bamford"만을 리턴할 것이라고 값을 좁혀준다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
interface Joke { quote: string; style: "story" | "one-liner"; } function tellJoke(joke: Joke) { // 변수 joke는 Joke인터페이스를 참조한다. if (joke.style === "one-liner") { console.log(joke.quote); } else { console.log(joke.quote.split("|n")); } } const narrowJoke = { quote: "too long sentence", style: "one-liner" as const, }; tellJoke(narrowJoke); // OK // 왜 OK일까 const wideObject = { quote: "also too long sentence", style: "one-liner", // string 타입이기 때문에 }; tellJoke(wideObject); // Error: Argument of type "{quote: string; style: string;}" // is not assignable to parameter of type "LogAction" // Types of propery 'style' are incompatible. // Type 'string' is not assignable to type "story"|"one-liner".
9.5.2 읽기 전용 객체
as const
를 사용해 리터럴을 어서션하면 유추된 타입이 구체적으로 전환된다.- 고유한 리터럴 타입으로 간주되며, 배열은 읽기 전용 튜플이 된다.
리터럴에
const
어서션을 적용하면 해당 리터럴의 모든 멤버에 동일한const
어서션이 재귀적으로 적용된다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
function describePreference(prefenrence: "maybe" | "no" | "yes") { switch (prefenrence) { case "maybe": return "I suppose..."; case "no": return "No thanks."; case "yes": return "Yes plz"; } } const preferencesMutable = { movie: "maybe", standup: "yes", }; describePreference(prefenrencesMutable.movie); // Error: Argument of type 'string' is not assignable to // parameter of type '"maybe"|"no"|"yes"'. prefenrencesMutable.movie = "no"; // OK const preferencesReadonly = { movie: "maybe", standup: "yes", } as const; decrementPreference(prefenrencesReadonly.movie); // OK prefenrencesReadonly.movie = "no"; // Error: Cannot assign to 'movie' because it is a readonly property.