TypeScript - 고급 타입2

euNung·2022년 7월 27일
0

TypeScript

목록 보기
7/8

조건부 타입

: 타입에 삼항연산자(조건연산자) 적용

type IsString<T> = T extends string ? true : false

type A = isString<string>				// true
type B = isString<number> 				// false
  • 분배

    string extends T ? A : B === string extends T ? A : B
    
    (string | number) extends T ? A : B === (string extends T ? A : B) 
    										| (number extends T ? A : B)
    • 유니온 타입에 적용

      // 조건부 타입x
      type ToArray<T> = T[]
      type A = ToArray<number>				// number[]
      type B = ToArray<number | string>		// (number | string)[]
      
      // 조건부 타입o
      type ToArray2<T> = T extends unknown ? T[] : T[]
      type A = ToArray2<number> 				// number[]
      type B = ToArray2<number | string>		// number[] | string[]
    • 다양한 공통 연산을 안전하게 표현 가능

      type Without<T, U> = T extends U ? never : T
      
       type A = Without<boolean | number | string, boolean>		// number | string
       
      //---과정 ---//
       type A = Without<boolean, boolean>
      		  | Without<number, boolean>
      		  | Without<string, boolean>
      
      	    = (boolean extends boolean ? never : boolean)
      	      | (number extends boolean ? never : boolean)	      
      		  | (string extends boolean ? never : boolean)
      		
      		= never 
      		  | number
      		  | string
      
      		= number | string
  • infer 키워드
    : 조건부 타입에서 제네릭 타입을 인라인으로 선언하는 전용 문법

    // inter 적용x
    type ElementType<T> = T extends unknown[] ? T[number] : T
    type A = ElementType<number[]> 		// number
    
    // infer 적용o
    type ElementType2<T> = T extends (infer U)[] ? U : T
    type B = ELementType2<number[]>		// number
    
    // 인라인으로 선언x 경우
    type ElementUgle<T, U> = T extends U[] ? U : T
    type C = ElementUgly<number[]> 		// Error: 제네릭 타입 'ElementUgly'는 
    									// 		  두 개의 타입 인수를 필요로 함
    • 복잡한 예제

      // 두번째 인수의 타입 얻기
      type SecondArg<F> = F extends (a: any, b: infer B) => any ? B : never
      
      // Array.slice의 타입 얻기
      type F = typeof Array['prototype']['slice']
      
      type A = SecondArg<F>		// number | undefined
  • 내장 조건부 타입들

  Exclude<T, U> 		// 앞서 작성한 Without 타입처럼 T에 속하지만 U에는 없는 타입을 구함
  	type A = number | string
	type B = string
	type C = Exclude<A, B>		// number


  Extract<T, U>			// T의 타입 중 U에 할당할 수 있는 타입을 구함
    type A = number | string
	type B = string
	type C = Extract<A, B>		// string


  NonNullable<T>		// T에서 null과 undefined를 제외한 버전을 구함
   type A = {a?: number : null}
   type B = NonNullable<A['a']>	// number


  ReturnType<F>			// 함수의 반환 타입을 구함(제네릭과 오버로드된 함수에서는 동작x)
    type F = (a: number) => string
	type R = ReturnType<F>		// string


  InstanceType<C>		// 클래스 생성자의 인스턴스 타입을 구함
    type A = {new(): B}
    type B = {b: number}
	type I = InstanceType<A>	// {b: number}

어서션

  • 타입 어서션
    : 타입 B가 있고 A <= B <= C를 만족하면 타입 검사기에게 B는 실제로 A거나 C라고 어서션할 수 있음
function formatInput(input: string) { ... }
function getUserInput(): string | number { ... }

let input = getUserInput()

formatInput(input as string)
formatInput(<string>input)                                          
  • Nonnull 어서션

    type Dialog = { id?: string }
    
     function closeDialog(dialog: Dialog) {
     	if(!dialog.id) { return }
        // === 여기서 어떤 코드를 통해 dialog가 변경될 수 있음 === //
       
     	// 화살표 함수 내부이므로 유효범위가 바뀌면서 if문을 통한 정제가 무효화됨
     	setTimeout(() => 
       	 removeFromDom(dialog, document.getElementById(dialog.id))		// Error: 'string | undefined' 타입의 인수는
                														// 'string' 타입의 매개변수에 할당할 수 없음
     	)
      }
    
     function removeFromDOM(dialog: Dialog. element: Element) {
     	element.parentNode.removeChild(element)							// Error: 객체가 'null'일 수 있음
     	delete dialog.id
     }
    
      // Nonnull 어서션 연산자(!)를 사용하여 해결
     function closeDialog(dialog: Dialog) {
     	if(!dialog.id) { return }
        setTimeout(() => 
        	removeFromDom(dialog, document.getElementById(dialog.id!)!)
        )
      }
    • 어서션을 사용하지 않고 정제

      type VisibleDialog = {id: string}
       type DestoryedDialog = {}
       type Dialog = VisibleDialog | DestoryedDialog
      
       function closeDialog(dialog: Dialog) {
        // dialog는 VisibleDialog 타입이 됨
       	if(!('id' in dialog) { return }
        	// 화살표 내부의 dialog는 외부의 dialog와 같은 값 => 정제 이어짐
        	setTimeout(() => 
          	removeFromDOM(dialog, document.getElementById(diablog.id)!)
          )
       }
      
       function removeFromDOM(dialog: VisibleDialog, element: Element) {
         	element.parentNode!.removeChild(element)
         	delete dialog.id
       }
                       
    • 확실한 할당 어서션

      let userId: string
      userId.toUpperCase()		// Error: 할당하지 않고 'userId' 변수를 사용함
      
      // 어서션을 사용하여 타입스크립트에 변수에 값이 반드시 있다는 것을 알려줌
      let userId!: string
      fetchUser()
      
      userId.toUpperCase()
      
      function fetchUser() {						// 반드시 userId가 정의됨을 보장한다고 가정
       	userId = globalCache.get('userId')		
      }

이름 기반 타입 흉내내기

: 타입스크립트는 구조 기반 타입
=> 타입의 구조만 같다면 에러 발생x

type CompanyId = string
type OrderId = string
type UserID = string
type ID = CompanyID | OrderId | UserId

function queryForUser(id: UserId) {
  ...
}
  
  let id: Company = 'b12345678'
  queryForUser(id)				// Error 발생 안함
  • 브랜디드 타입(branded type)을 이용하여 이름 기반 타입 흉내내기
type CompanyId = string & { readonly brand: unique symbol }
type OrderId = string & { readonly brand: unique symbol }
type UserId = string & { readonly brand: unique symbol }
type ID = CompanyId | OrderId | UserId

// 타입 어셔션(as)를 사용하여  브랜디드 타입의 생성자 생성
function CompanyID(id: string) {
  return id as CompanyID
}

function OrderID(id: string) {
  return id as OrderId
}

function UserID(id: string) {
  return id as UserID
}

function queryForUser(id: UserID) {
  ...
}
  
let companyID = CompanyID('a1234567')
let orderId = OrderID('qwer123')
let userId = UserID('zcxv45678')

queryForUser(userId)
queryForUser(companyId)			// Error: 'Compnay ID' 타입의 인수를 'UserID' 타입의 매개변수에 할당할 수 없음

프토토타입 안전하게 확장하기

  1. .ts 파일에서 프로토타입 확장

    // 전역 범위로 정의된 Array<T> 인터페이스에 zip 메서드 추가(인터페이스 합치기 기능 이용)
    interface Array<T> {
    	zip(U)(list: U[]): [T, U][]
    }
    
    // 만약 zip을 구현하는데 뭔가를 import 해야하는 상황이라면 
    // 전역 확장을 declare global이라는 타입 선언으로 감싸야 함
    declare global {
    	interface Array<T> {
     		zip(U)(list: U[]): [T, U][]
     	}
    }
  2. 새로운 메서드를 프로토타입에 추가

    function tuple<T extends unknown[]<(...ts: T):T {
    	return ts
    }
    
    // this 타입을 사용하여 타입스크립트가 .zip이 호출되는 대상 배열에서 
    // T 타입을 올바로 추론할 수 있도록 함
    Array.prototype.zip = function <T, U>(this: T[], list: U[]): [T, U][] {
    	return this.map((v, k) => tuple(v, list[k]))
    }
  3. Array.prototype에 기능을 추가하려면 zip을 사용하는 모든 파일이 zip.ts를 먼저 로드해야함
    => 프로젝트에서 zip.ts를 명시적으로 제외하도록 tsconfig.json 수정
    => 이 기능을 사용하는 쪽에서 명시적으로 import 해야함

    {
    	* exclude *: [
       		"./zip.ts"
     	]
    }
  4. 다음과 같이 사용

    import './zip'
    
    [1, 2, 3].map(n => n * 2)			// number[]
    		.zip(['a', 'b', 'c'])		// [number, string][]
profile
프론트엔드 개발자

0개의 댓글