- 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는 자바스크립트 코드를 통해서도 타입이 제어가되기 때문에, 런타임에 영향을 주지만, 사용자 정의 함수를 통한 타입은 런타임에 영향을 주지 않습니다. 가장 간단한 타입 좁히기 방법으로는 자바스크립트 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 입니다. // 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} // 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 | Square function getArea(diagram:Diagram) { switch(diagram.name) { case "circle": return diagram.radius 3.14; // Circle case "square": return diagram.width default: return 0; } } 목차 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 | LayoutError if (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 | LayoutError if (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 | LayoutError assertLayoutError(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]; // string type Litral = typeof data2[keyof typeof data2]; // "carrot"
2025-04-050013 - Typescript
[Typescript] Template Literal Types
[Typescript] Template Literal Types Typescript 기능들을 하나씩 다뤄볼 예정입니다. 그 첫 번째 기능으로 당첨된 것은 Template Literal Types입니다. literal types 자체는 어려운 내용이 아니지만, 2.활용 예시는 Generic Type과 Type infer 대해 알고 있어야 이해가 가능할 것 같습니다. Template Literal 타입이란 문자열 리터럴을 템플릿 문자열 형식(백틱 ``)으로 만들어 조합하여 사용할 수 있게 해주는 타입입니다. Template Literal 타입과 String Literal 타입은 문자열 규칙에 따른 디자인 토큰 생성이나, Object의 key값에 따른 자동 타입 생성같이 다양한 상황에서 사용할 수 있습니다. 아래 각 타입에 대한 설명입니다. Literal Type : string, number 같이 일반적인 타입만이 아니라, 특정 문자열이나 숫자를 타입으로 지정한 타입입니다. type Direction = "left" | "right"; const direction:Direction = "left"; ✅ const direction:Direction = "up"; ❌ // left와 right 값만 가질 수 있다. Type '"up"' is not assignable to type '"left" | "right"' const direction:string = "up"; ✅ // string 타입은 모든 문자열이 가능하다. Template Literal : String Literal을 템플릿 문자열 형식(백틱 ``)으로 조합하여 만드는 타입입니다. type Fruit = "apple" | "banana" | "mango"; type Sentence = `I like ${Fruit}`; const sentence:Sentence = "I like apple"; ✅ const sentence:Sentence = "I hate apple"; ❌ // I like 가 아니다. Type '"I hate apple"' is not assignable to type '"I like apple" | "I like banana" | "I like mango"'. const sentence:Sentence = "I like melon"; ❌ // Fruit 타입에 없는 과일이다. Type '"I like melon"' is not assignable to type '"I like apple" | "I like banana" | "I like mango"'. Template Literal을 어느 상황에 사용할 수 있는지 예시로 보여드리겠습니다. 저가 Template Literal을 사용했던 예시 1가지와, 공식 문서의 활용 예시 1가지를 가져와 봤습니다. 2.1 디자인 토큰 저는 이 기능을 디자인 시스템에서 {type}-{Size}-{Weight}의 규칙을 가진 Typography 디자인 토큰을 정의하는데 활용했었습니다. 아래 그 예시 코드입니다. type FontType = "title" | "body"; type FontSize = "sm" | "md" | "lg"; type FontWeight = "regular" | "medium" | "bold"; type FontVariant = `${FontType}-${FontSize}-${FontWeight}`; // FontType 2가지 FontSize 3가지 // "title-sm-regular", "title-md-regular", "title-md-bold", "title-md-regular...등등 18가지 문자열 유니온 타입이 만들어집니다. type FontProperties = Pick; type Typography = Record; const typography: Typography = { "body-lg-bold": { fontSize: "1rem", fontWeight: 700, letterSpacing: 0, lineHeight: 1, }, "body-lg-medium": { fontSize: "1rem", fontWeight: 500, letterSpacing: 0, lineHeight: 1, }, //... }; 만약 Typography 타입에 대해 더 타이트하게 적용하고 싶다면 응용할 수 있습니다. 다음은 FontWeight에 따라 자동으로 CSS fontWeight 타입이 고정되는 예시입니다. import {CSSProperties} from "react"; type FontType = "title" | "body"; type FontSize = "sm" | "md" | "lg"; type FontWeightValueMap = { regular: 400; medium: 500; bold: 700; };// 만약 변수로도 사용하고 싶으면 const fontWeightValueMap으로 변수로 만들어 사용. typeof 지시어로 타입 호출. type FontWeight = keyof FontWeightValueMap; // "regular" | "medium" | "bold" type FontWeightCSSProperty = { fontWeight: FontWeightValueMap[FW]; }; //FontWeight에 따라 값 CSS literal type number로 고정. type FontVariant = `${FontType}-${FontSize}-${FontWeight}`; // 18가지 토큰 type ExtractFontWeight = FV extends `${string}-${string}-${infer FW}` ? FW : never; // FontVariant에서 FontWeight 추출 type FontProperties = Pick & FontWeightCSSProperty>; //fontWeight는 Font Variant의 Font Weight에 따라 값 결정. type Typography = { [key in FontVariant]: FontProperties; }; const typography: Typography = { "body-lg-bold": { fontSize: "1rem", fontWeight: 700, ✅ letterSpacing: 0, lineHeight: 1, }, "body-lg-medium": { fontSize: "1rem", fontWeight: 700, ❌ medium은 500의 값을 가져야 하기 때문에 타입 에러. Type '700' is not assignable to type '500' letterSpacing: 0, lineHeight: 1, }, //... }; 디자인 시스템에서 두 가지 타입 설정 방법 중에 뭐가 더 낫냐고 물으신다면 어떤 이점을 가지고 싶은 지 혹은 상황에 따라 다릅니다. 디자인 토큰 구조를 더 이상 건드릴 예정도 없거나 빠르게 끝낼 프로젝트라면 타입에 신경 쓰는 것은 시간 낭비 밖에 안됩니다. 하지만 중요하게 쓸 디자인 시스템으로 피그마까지 연동하여 버전 관리과 자동 Figma 반영까지 고려했다면 명확한 디자인 기획에 따라 타입을 설정해주는 것이 더 좋아 보입니다. 결론은 저의 생각으로는 가용 인력, 숙련도, 여유 시간 등 상황에 따라 정하는게 좋아 보입니다. 2.2 공식 문서 예시 아래는 공식문서의 object의 literal 키 값과 그 value의 타입에 따라 함수의 타입이 자동으로 만들어지는 예시입니다. object의 value 타입이 다양하더라도 개발자는 편하게 매칭된 타입으로 코딩을 할 수 있습니다. type PropEventSource = { on (eventName: `${Key}Changed`, callback: (newValue: T[Key]) => void): void; }; // ${T의 key 값} + "Changed"가 eventName이 된다. callback의 newValue는 ${T의 key 값에 매칭되는 value의 타입이된다} /// Create a "watched object" with an `on` method /// so that you can watch for changes to properties. declare function makeWatchedObject(obj: T): Type & PropEventSource; const person = makeWatchedObject({ firstName: "Saoirse", lastName: "Ronan", age: 26 }); person.on("firstNameChanged", (newName) => {console.log(newName)}); // on함수의 1번째 파라미터는 `{key}Changed`이 되고, 2번째 파라미터 함수의 newName은 자동으로 string 타입이 된다. person.on("ageChanged", newAge => {console.log(newAge)}) // 마찬가지로 ${person의 key 값 = "age"}+"Changed", newAge는 person["age"]의 타입이다. 아래는 Template Literal, String Literal과 관련된 유틸리티 타입들 입니다. 공식 문서에서 가져왔습니다. 3.1 Uppercase 모든 글자를 대문자인 타입으로 변환해주는 유틸리티. type Greeting = "Hello, world" type ShoutyGreeting = Uppercase // "HELLO, WORLD" 타입 type ASCIICacheKey = `ID-${Uppercase}` type MainID = ASCIICacheKey // "hello, world" 타입 type ASCIICacheKey = `id-${Lowercase}` type MainID = ASCIICacheKey // "Hello, world" 타입 3.4 Uncapitalize 첫 글자를 소문자인 타입으로 변환해주는 유틸리티 type Greeting = "HELLO WORLD" type ShoutyGreeting = Uppercase // "hELLO WORLD" 타입
2025-04-040014