[ Language > Typescript ]
[Typescript] Type Narrow, CFA
[Typescript] Type Narrow & CFA
타입스크립트는 "string" | "number"같은 유니온 타입에 대해 타입을 CFA(Control Flow Analysis)와 사용자 정의 함수를 통해서 Type Narrow하여 그 결과를 개발자에게 보여줍니다.
Control Flow는 한국어로 제어 흐름이고 프로그램에서 실행되는 각 구문, 명령어나 함수가 호출되는 순서를 의미합니다. 제어 흐름을 나타내는 구문은 어떤 조건에 따라 실행 하거나, 생략하거나를 결정하여 호출 순서 혹은 흐름을 바꿀 수 있도록 도와주는 구문으로 제어문이라고 합니다. 대표적인 boolean logic 제어문으로는 if, switch, try 같은 구문이 대표적입니다. 순수 자바스크립트의 코드를 분석하는 것과 같습니다.
사용자 정의 함수는 "is", "assert", "infer", "as"같은 키워드나 타입 Generic을 사용한 타입 혹은 타입 함수입니다. CFA를 통해서 타입이 결정되는 것과 다른 점은, 타입스크립트의 키워드들은 컴파일 후에는 다 사라집니다.
핵심은 CFA는 자바스크립트 코드를 통해서도 타입이 제어가되기 때문에, 런타임에 영향을 주지만, 사용자 정의 함수를 통한 타입은 런타임에 영향을 주지 않습니다.
1. Type Narrow by natural javascript [If 제어문]
가장 간단한 타입 좁히기 방법으로는 자바스크립트 If문과 자바스크립트 타입 연사자가 있습니다. 타입 연산자 종류는 크게 3가지가 있습니다. typeof, in, instanceOf입니다. 각각 Primitive+ built-in Type, Object, Class를 구별하는데 사용합니다. 구별하고자 하는 변수 종류에 따라 타입을 좁히는 방법이 다릅니다.
1.1 Primitive & built-in Type Narrow (typeof 연산자)
Primitive 값에는 string, number, boolean, symbol 등이 있습니다. 그리고 이 타입들은 typeof 연산자를 사용하여 변수의 타입을 알아내여 if문으로 타입을 좁힐 수 있습니다. "object"같은 built-in 자바스크립트 타입도
declare const getUserAge: () => undefined | number;const age = getUserAge(); // undefined | number;if (typeof age === 'undefined') return; // age가 undefined면 return을 합니다 => 다음 줄 부터 union 타입에서 제거됩니다.const koreanAge = age + 1; // number;
1.2 Object Type Narrow (in 연산자)
두 개의 Object가 있을 때, 두 개의 Object가 같은지 확인하려면 같은 Property가 있는지 확인해야 합니다. 자바스크립트에서 이를 확인하려면 직접 해당하는 Property Key값을 가지고 있는지 확인해야 합니다. 이 기능을 위한 연산자가 in 입니다.
// 1. 서로 다른 객체 key값 비교declare const getUserAge: () => {value: number} | {error: Error};const age = getUserAge(); // {value: number} | {error: Error};if ("error" in age) return; // "error"가 property로 있으면 종료.const koreanAge = age.value + 1; // {value: number}// 2. 서로 다른 객체 key & value type 비교.declare const getUserAge: () => | {value: number} | {value: undefined} | {error: Error};const age = getUserAge(); // {value: number} | {value: undefined} | {error: Error};if ("error" in age) return; if (typeof age.value === 'undefined') return; // {value: number} | {value: undefined}const koreanAge = age.value + 1; // {value: number}
1.3 Class Type Narrow (instanceof 연산자)
클래스는 상속을 받아 확장할 수 있습니다. 상속 받은 클래스가 SubClass, 상속해주는 클래스가 SuperClass가 됩니다. 그리고 이 SubClass로 생성된 객체는 SubClass의 인스턴스이기도 하지만, SuperClass의 인스턴스도 될 수 있습니다. instanceof 연산자는 인스턴스의 프로토타입 체인중에 Class의 프로토타입과 같은 객체가 있는지 확인하기 때문에, 인스턴스의 생성자 클래스의 모든 SuperClass와 비교할 경우 true가 되게 됩니다. 그리고 타입스크립트는 instanceof와 if를 분석하여 Type Narrow를 수행합니다.
instanceof 판단 기준 도움 코드.ts
class User { age: number; constructor(age: number) { this.age = age; }}class MVPUser extends User { money: number; constructor(age: number, money: number) { super(age); this.money = money; }}const commonUser = new User(20);const richUser = new MVPUser(21, 1000);if (richUser instanceof User) { console.log("i am user") // 출력}if (commonUser instanceof User) { console.log("i also user") // 출력}if (commonUser instanceof MVPUser) { console.log("am i rich user?") // X}if (richUser instanceof MVPUser) { console.log("i am rich user") // 출력}if (commonUser instanceof Object) { console.log("i also Object") // 출력}if ('age' in richUser) { console.log("i also object2") // 출력}
declare const getNumberOrNumbers: () => number | number[];const value = getNumberOrNumbers(); // number | number[]if (value instanceof Array) { console.log(value .length); // number[]} else { console.log(value .toPrecision(0)) // number}
1.4 자바스크립트 기본 type-guard 함수
Array 클래스와 isArray 함수가 예시 중 하나인 기본 type-guard 함수입니다.
declare const getNumberOrNumbers: () => number | number[];const myNum = getNumberOrNumbers(); // number | number[] ;if (Array.isArray(myNum)) { console.log(myNum.length); // number[]} else { console.log(myNum.toPrecision(0)) // number}
1.5 비교 연산자로 Union type narrow 하기.
첫 번째 예시는 litral type을 if문과 비교 연산자로 narrow한 것이고, 두 번째 예시는 name이라는 같은 key를 공유하는 Object들 중에서도, 그 litral 값을 이용하여 type narrow를 한 예시입니다.
litral type narrow.ts
declare const getSomething: () => "한글1" | "한글2" | 3;const something = getSomething(); // number | number[] ;switch (something) { case "한글1": console.log(something + "스트링"); // "한글1" break; case "한글2": console.log(something + "스트링2"); // "한글2" break; default: console.log(something + 10); // 3}if (something === 3) { console.log("숫자", something) // 3} else { console.log("문자", something + "스트링3") // "한글1" | "한글2"}
Discriminated Unions.ts
type Circle = { name: "circle"; radius: number;}type Square = { name: "square"; width: number;}type Diagram = Circle | Squarefunction getArea(diagram:Diagram) { switch(diagram.name) { case "circle": return diagram.radius * diagram.radius * 3.14; // Circle case "square": return diagram.width * diagram.width; // Square default: return 0; }}
2. 타입 가드(Type Guards), 타입 단언(Assertion),
목차 1에서 했던 것들이 사실 모두 타입 가드들입니다. 타입 가드는 여러개의 타입이 가능한 변수에서 그 타입을 좁혀나가는 것입니다.
아래서 설명드릴 타입 가드는 사용자 정의 타입가드 입니다. type predict(타입 판별) 키워드 "is"를 사용합니다.
타입 단언(표명)은 반드시 어떤 타입이어야 한다는 것을 선언하는 것입니다. 코드 또한 해당 변수를 확인하여 원하는 타입이 아니면 과감하게 Error를 던지는 식으로 만들어야 탄탄하게 타입을 관리할 수 있습니다. as 또는 assert 지시어를 사용하여 단언할 수 있습니다. ThirdParty를 사용할 경우 타입 정의 없어서 어쩔 수 없이 사용하는 경우 많이 사용합니다. 혹은 타입이 너무 복잡해 진다면 그냥 as를 쓰는 것도 개발 비용을 줄이는데 도움이 된다고 생각합니다.
Type Guard (사용자 정의)
사실 instanceof를 사용하면 obj is LayoutErroor로 안써도 알아서 CFA를 통해 추론이 됩니다.
declare const getError: () => NetworkError | LayoutError;class NetworkError extends Error { retryCount: number;};class LayoutError extends Error { target: string;};function isLayoutError(obj: Error): obj is LayoutError { return obj instanceof LayoutError;}const error = getError(); // NetworkError | LayoutErrorif (isLayoutError(error)) { console.error(error.target,"여기서 레이아웃 에러") //LayoutError} else { console.error("재시도 횟수 :", error.retryCount) //NetworkError}
혼란스러운 Type Guard
타입 가드는 LayoutError로 해야했는데, 이상하게 해서 바껴버린 상황입니다. TS Error 없이 작동합니다.
declare const getError: () => NetworkError | LayoutError;class NetworkError extends Error { retryCount: number;};class LayoutError extends Error { target: string;};function isLayoutError(obj: Error): obj is NetworkError { return obj instanceof LayoutError;}const error = getError(); // NetworkError | LayoutErrorif (isLayoutError(error)) { console.error(error.target,"여기서 레이아웃 에러") // NetworkError개발자가 obj is NetworkError라고 해버려서, 타입 추론 결과가 바껴버려 NetworkError 타입으로 됐습니다.} else { console.error("재시도 횟수 :", error.retryCount) // LayoutError}
Assertion Function (asserts)
assert를 사용했으면, 해당 함수 실행 후에는 반드시 타입이 assert한 타입으로 자동 추론됩니다. 그렇기 때문에 만약 자바스크립트 검사 결과가 아닐경우 아에 Error를 띄어버리는 코드를 넣었습니다. 없어도 작동은 하지만 위와 같이 타입 안정성 보장은 안됩니다.
declare const getError: () => NetworkError | LayoutError;class NetworkError extends Error { retryCount: number;};class LayoutError extends Error { target: string;};function assertLayoutError(obj: Error): asserts obj is LayoutError { if (!(obj instanceof LayoutError)) throw new Error("반드시 LayoutError가 온다 했자나요!!")}const error = getError(); // NetworkError | LayoutErrorassertLayoutError(error); console.log(error.target) // LayoutError;
Assertion (as)
// 예시 1 일반전인 타입 단언.interface fruite { name: string; price: number;}var apple = {} as fruite;// 예시 2 MouseEvent는 Event의 Subclass이다. 타입스크립트가 인정해주는 타입 단언이라 에러 표시가 안난다.(DownCasting)function onClick (event: Event) { let mouseEvent = event as MouseEvent;}// 예시 3 다운캐스팅, 업캐스팅도 아니기 때문에, 그냥 형변환 하면 TS Error 메시지가 나타난다. unkown으로 우회하여 변환해야한다.// 물론 tsConfig 설정에 따라 다르다interface Message { content : string;}function onReceive(msg: Message) { let messageReceived = event as unkown as string;}
as const
as const는 Object의 프로퍼티들을 non-literal한 key-value타입으로 확장되는 것을 막아주는 역할을 합니다. 간단히 말하면 Object를 literal 버전으로 만듭니다.
const data1 = { name: "carrot"};const data2 = { name: "carrot"} as const;type Common = typeof data1[keyof typeof data1]; // stringtype Litral = typeof data2[keyof typeof data2]; // "carrot"