TIL

241127 TIL

GoJay 2024. 11. 27. 23:48
  • 타입스크립트 공부(공식 문서)
    • 유니언 타입으로 값을 받으면 실제로 들어온 값의 타입이 유니언 타입 중 어디에 해당하는지를 몰라 문제가 생길 수 있다. 이럴 경우 자체적으로 타입을 구분하는 분기 처리를 하여 타입에 맞는 사용을 해야 하는데, 이때 사용되는 분기문을 '타입 가드(Type Guard)'라고 부른다.
    • 타입을 좁히기 위해 자바스크립트의 비교 연산이 사용되며, Truthy-Falsy에 대한 이해가 필요하다. 타입에 해당하는 값을 true 또는 false로 만들기 위한 적절한 조건을 잘 세워야 하는 것이다.
    • Truthy-Falsy 값에 부정 연산자(!)를 이중(!!)으로 사용할 경우 단순히 값이 Boolean 타입으로 변환되는 게 아니라, true 또는 false 값을 사용한 리터럴 타입으로 변환된다. 이는 적절한 타입 가드를 만드는 데 있어 중요한 역할을 한다.
    • 적절하게 타입 가드를 사용하고, 타입을 좁히기 위해 if switch in instanceof is 등 분기 처리와 조건 연산을 위한 명령들을 잘 확인해두는 게 필요하다.
    • 만약에 변수에 유니언 타입을 let x = string | number와 같이 할당했다면, 변수 x는 코드 흐름 상 문자열과 숫자형 값 모두를 저장할 수 있는 것처럼 동작한다. 하지만, 이는 타입이 사용되지 않는 게 아니라, 문자열 값이 할당되면 그 시점에 문자열로, 숫자가 할당되면 그 시점에 숫자형 타입으로 변환하는 것이다. 변수에 값을 할당할 수 있을지 여부는 언제나 선언 시점의 타입으로 결정된다(선언 시점에 저장된 타입 정보를 들고 있는 것처럼 동작한다).
    • 변수가 선언 시점의 타입 정보를 물고 있기 때문에, 코드 흐름에 따라서는 하나의 변수가 각 지점에서 서로 다른 타입을 갖고 있는 것처럼 보일 수 있다.
    • 객체의 프로퍼티 타입을 기준으로 분기처리 해야할 때, 어떤 타입 정의인지를 구분하기 위해 구분자가 되어줄 프로퍼티를 추가하고(e.g. interface Circleinterface Squarekind 프로퍼티), 해당 프로퍼티를 기준으로 분기를 나눠 안전하게 처리할 수 있다.
    • 만약에 타입을 좁힌 모든 분기처리에 해당하지 않고, 분기문에 해당하는 타입이 없다고 판별될 시 에러를 발생시키는 게 필요하다(에러가 발생해야 어디서 뭐가 잘못됐는지 알 수 있다). 예를 들어, switch 케이스로 작성된 문에서 default 부분에 에러를 발생시키는 코드를 넣는 게 필요할 수 있는데(모든 case에 해당하지 않을 경우 에러 발생), 이때 never 타입을 활용할 수 있다.
    • never는 모든 타입의 서브 타입이기 때문에 어떠한 타입에도 할당할 수 있지만, 어떠한 타입도 never 타입인 변수에 할당될 수는 없다. 이를 이용해서 const _exhaustiveCheck: never = shape 형태로 사용해 일부러 에러를 만들 수 있다.
    • throw new Error도 에러 객체를 통해 에러를 발생시키지만, 이는 런타임에 에러를 뱉는다. 타입스크립트는 런타임 이전인 컴파일 시점에 에러를 발생시켜 좀 더 안정적인 프로그램을 만들도록 하는 게 목적이기 때문에, throw new Error 보다는 명시적으로 컴파일 시 에러가 날 수 있는 구문을 추가해 주는 게 적절하다.
    • '시그니처'란 함수나 메서드, 콜백의 구조를 정의하는 규칙을 의미한다. 즉, 함수의 이름이 뭐고, 어떤 타입의 매개변수를 받고, 어떤 타입의 반환값을 갖는지를 정의해 둔 형태를 의미한다.
    • 함수의 시그니처를 표현할 수 있는 방법은 다양하다. 먼저, 함수 타입 표현식이 있다. 시그니처를 표현하기 위해 (str: string) => void와 같은 문법을 사용한다. 해당 코드의 의미는 'str이라는 문자열 형태의 매개변수를 값으로 받고, 반환값은 없거나 undefined인 함수'를 뜻한다.
    • 함수 타입 표현식을 별칭을 사용해 처리할 수도 있다. type GreetFunction = (a: string) => void;와 같이 타입을 먼저 정의하고, function greeter(fn: GreetFunction) {} 또는 const greeter: GreetFunction = (fn) => {} 형태로 사용 가능하다.
    • '콜(Call; 호출) 시그니처'는 객체와 유사한 형태로 파라미터와 함수의 타입 정의를 함께 사전에 정의내릴 수 있다. 아래와 같은 방식으로 활용된다.
    type DescribableFunction = {
      description: string;
      (someArg: number): boolean; // 함수 타입 정의, 함수 타입 표현식이 =>를 쓴 것과 달리 :를 사용하고 있는 점 주의하자
    };
    
    function doSomething(fn: DescribableFunction) {
      console.log(fn.description + " returned " + fn(6));
    }
    • 함수의 타입을 정의한 (someArg: number): boolean; 코드 말고, description 이라는 게 정의된 게 보인다. 해당 값은 해당 타입 정의를 사용한 함수의 프로퍼티가 된다. 자바스크립트에선 함수도 객체이기 때문에 프로퍼티를 가질 수 있다. 콜 시그니처의 타입 정의 안에 정의해 둔 함수의 타입 선언 이외의 것들은 함수의 프로퍼티가 된다.
    • 콜 시그니처 앞에 new 키워드를 붙이면 생성자(Construct) 시그니처가 된다. 해당 시그니처로 만들어진 함수는 객체를 반환하게 된다.
    type SomeConstructor = {
      new (s: string): SomeObject;
    };
    function fn(ctor: SomeConstructor) {
      return new ctor("hello");
    }
    • 객체 타입을 정의할 땐 몇 가지 추가적인 정보를 제공하는 방법이 있다.
      • Optional(선택적) 프로퍼티 여부: alias로 객체를 만들 때(또는 객체를 직접 선언할 때) 프로퍼티와 값을 구분하는 :?를 붙여주면(e.g. { name ?: string }) 해당 프로퍼티는 옵셔널 하게 된다. 옵셔널 하다는 것의 뜻은 있어도 되고 없어도 된다는 것이다. 옵셔널 프로퍼티는 타입스크립트에 의해 전달한 타입(string)과 undefined의 유니언 타입으로 연산되며(선택적으로 해당 값을 입력하지 않으면 undefined가 반환됨), 타입 가드를 통해 undefined의 방어 로직을 잘 세워줘야 한다(tsconfig.json 파일에 strictNullCheckstrue로 설정해주면 옵셔널 프로퍼티에 대해 에러 발생 가능성이 주의가 뜬다).
      • readonly: 객체의 특정 프로퍼티를 readonly로 설정해 수정이 변경하도록 설정할 수 있다. { readonly name: string } 형태로 사용한다. readonly는 타입스크립트의 타입 검사에서 고려되지 않기 때문에, readonly name: stringname: string은 타입스크립트 입장에선 같은 타입인 셈이다. 그래서, readonly로 객체 프로퍼티의 변경을 막았어도 복제된 객체가 readonly가 아닌 상태라 수정이 되면 참조가 같은 이유로 값이 바뀔 수 있다. 아예 객체를 수정 불가한 동결 상태로 만들기 위해선 Object.freeze 등을 고려해야 한다.
      • 인덱스 시그니처: 객체에 정의돼야 하는 프로퍼티가 주로 비슷한 형태인데(유사한 타입을 갖는데) 많은 프로퍼티를 하나씩 다 쓰는 게 번거롭다면 인덱스 시그니처를 사용할 수 있다. 사용 방법은 { [key: string]: number }와 같다. key에는 string을 지정했기 때문에 문자열만이 올 수 있고(string, number, boolean 값을 설정할 수 있고, 실제론 전부 문자열로 변환되며, 타입스크립트의 타입은 any가 된다), 프로퍼티의 값으론 숫자형만이 가능하다. 해당 선언은 프로퍼티 명이 문자열이고, 값이 숫자인 모든 값을 허용한다. 만약에 적은 수의 프로퍼티만 예외적으로 값에 문자열을 받아야 한다면 아래와 같이 일부 프로퍼티만 별도로 타입을 명시적으로 정의해 줄 수 있다.
      type Person = {
        [key: string]: number;
        name: string,
        location: location
      }
    • 객체는 타입으로 정의한 프로퍼티 이외의 프로퍼티가 사용되면 '초과 프로퍼티 검사'를 수행한다. 별칭을 사용한 타입 정의에서 정의하지 않은 프로퍼티가 객체에 포함되어 있으면 타입스크립트는 이를 '초과 프로퍼티'라고 생각하고 에러를 뱉는다. 초과 프로퍼티 검사를 우회하고 싶다면 인덱스 시그니처를 사용하거나, 아니면 다른 객체를 변수에 선언-할당한 후 변수를 할당하는 것이다. 변수를 할당하면 초과 프로퍼티 검사에서 제외된다(아래 코드는 컴파일 시 에러가 발생하지 않는다).
    type Person = {
      name: string;
      age: number;
    }
    
    const person1 = {
      name: 'Jay',
      age: 20,
      weight: 60
    }
    
    const person2: Person = person1
  • 타입스크립트 공부(인프런 <한 입 크기로 잘라먹는 리액트>]
    • 함수의 타입 호환을 활용할 땐 매개 변수의 타입 및 개수, 반환값의 타입 등을 비교한다.
    • 반환값이 호환되는지는 반환값의 타입에 따라 타입 위계에 따른 호환이 이루어진다. 예를 들어, 리터럴 숫자와 숫자형 데이터는 상호 호환이 이뤄지고, 방향은 리터럴 숫자 -> 숫자 방향이다(리터럴 숫자는 일반 숫자형 변수에 할당할 수 있지만, 숫자형 변수를 리터럴 숫자 변수에 할당할 순 없다).
    • 매개 변수의 개수가 같으면 (매개변수 타입 비교에 문제가 없는 한) 양방향으로 호환이 이루어진다. 만약에 한 쪽이 매개 변수가 더 적고, 포함당하는 형태라면, 매개 변수가 적은 함수 -> 매개 변수가 많은 함수 방향으로 할당이 가능하다(역은 불가능하다).
    • 매개 변수의 개수가 같을 때 타입이 서로 다르다면 타입 비교를 실행한다. 만약에 타입이 같다면 같은 타입으로 처리하고 호환이 잘 이루어진다.
    • 객체 타입 비교 시에는 프로퍼티의 포함 관계에 따라 호환이 결정된다. A가 B의 프로퍼티를 포함하는 관계(B의 프로퍼티 개수보다 A의 프로퍼티 개수가 더 많고, A에게 있는 프로퍼티와 일치하는 타입이 B의 타입 정의에도 있는 경우)라면 A -> B 방향으로 할당이 가능하다.
    • type Animal = Dog | Cat처럼 유니언 타입으로 타입을 정의하고, 함수 내에서 타입 가드를 통해 분기 처리로 Dog인 상황과 Cat인 상황에 맞게 로직 처리를 해줄 수 있다.
    function warning(animal: Animal) {
      if ('isBark' in animal) { // 강아지에게만 있는 `isBark` 프로퍼티로 강아지인지 판별
        // 강아지에 해당하는 로직
      } else if ('isScratch' in animal) { // 고양이에게만 있는 `isScratch` 프로퍼티로 고양이인지 판별
        // 고양이에 해당하는 로직
      }
    }
    • 해당 타입 가드는 undefined에 취약하다는 단점이 있다. 애초에 DogCatspecies 같은 프로퍼티를 추가한 다음 해당 프로퍼티 기준으로 분기 처리를 한다면 도움이 되겠지만, 그게 어려운 상황이라면 아래처럼 함수를 만들어서 사용할 수 있다.
    function isDog(animal: Animal): animal is Dog { // 반환값이 true이면 animal은 Dog 타입으로 간주됨
      return (animal as Dog).isBark !== undefined // 타입 단언으로 Dog 타입이 아닌 값에 isBark로 접근해도 에러 발생하지 않음
    }
    function isCat(animal: Animal): animal is Cat { // 반환값이 true이면 animal은 Dog 타입으로 간주됨
      return (animal as Cat).isScratch !== undefined // 타입 단언으로 Cat 타입이 아닌 값에 isScratch로 접근해도 에러 발생하지 않음
    }
    // ...
    function warning(animal: Animal) {
      if (isDog(animal) { // 강아지에게만 있는 `isBark` 프로퍼티로 강아지인지 판별
        // 강아지에 해당하는 로직
      } else if (isDog(animal) { // 고양이에게만 있는 `isScratch` 프로퍼티로 고양이인지 판별
        // 고양이에 해당하는 로직
      }
    }
    • 타입 단언(animal as Dog, animal as Cat)과 타입 조건(animal is Dog, animal is Cat)을 사용해 안정적으로 들어오는 값의 타입 정의가 무엇인지 검사하여 분기 로직을 돌릴 수 있다.
    • interfacetype처럼 타입 정의를 만들 때 사용한다.
    • interface를 사용하면 클래스에서 사용하는 extends 키워드를 사용해서 이전에 만든 타입 정의를 상속받을 수 있다.
    • 상속 시 기존에 있던 타입을 재정의해서, 덮어씌우는 것도 가능하다.
    • 한번 상속받아 확장한 타입 정의를 다시 한 번 상속받아 추가적인 확장을 하는 것도 가능하다(클래스와 많은 부분이 비슷한 것 같다).
    • 먼저 A라는 이름으로 정의된 interface를 한번 더 A라는 이름으로 다시 정의해도 문제가 되지 않는다. 두 정의에서 다루는 프로퍼티가 다르다면 A 인터페이스는 확장된다. 하지만, 이러한 방식의 확장은 extends와는 달라서, 먼저 정의된 타입을 변경하는 것은 안된다. 같은 프로퍼티를 다시 명기할 거면 이전의 프로퍼티 타입을 그대로 따라야 한다.

'TIL' 카테고리의 다른 글

241129 TIL  (2) 2024.11.29
241128 TIL  (1) 2024.11.29
241126 TIL  (0) 2024.11.27
241125 TIL  (0) 2024.11.26
241124 TIL  (0) 2024.11.25