저번 회고에 이어서 디지털하나로의 2번째 과목(?)인 TS를 회고해보려고 한다.
TS는 JS보다는 조금 짧게 진행했다.
약 7일 동안 배웠고, JS는 메모리랑 동작원리와 함께 진행했는데 지금은 이미 JS로 배웠기 때문에 다루지 않아서 더 짧게 한것 같다.

TS는 사실 제대로 공부하지는 않았고, 프로젝트할 때 사용해보긴한 상태였다.
그런데 타입을 지정하는게 아니라 내가 타입에게 잡아먹히면서 개발을 했다고 생각해서
이번 기회에 바로잡고 가고 싶었다.
이번에 수업 때 배우고, 또 야자하면서 같이 공부한 내용들을 정리하면서 회고를 해보겠다!!!!

Typescirpt 컴파일러

  1. Read TSConfig
    • 설정 파일을 읽음
    • 타겟 버전 지정
    • DOM
  2. Pre-process Files
    • 모든 ts파일을 모듈빼고 하나로 합침
    • 모듈은 모듈 스코프를 가지기 때문에
  3. Tokenize
    • 스캐닝
      • symbol: 스캐닝한 타겟
    • AST
      • Annotated
      • 타입이 붙음!
  4. Binder
    • AST를 읽으면 심볼이 떨어짐
    • x는 number고 1입니다를 심볼에 넣어 코드를 객체화
  5. Type Check
    • ex) number에 문자열 불가능!
  6. Transform
    • 타입을 다 만족하면 js로 트랜스폼
  7. Emit
    • *.js, `*.d.ts``
  • scaning하고 나면 AST가 생김

    const msg: string ="Hello, World!";
    
    • msg: 식별자
    • string: 타입
    • "Hello, World!": 리터럴
  • Binder

    • 스코프마다 타입이 변형됨

      const msg: string ="Hello, World!";
      welcome(msg);
      
      function welcome(str: string){
        console.log(str);
      }
      • welcome에는 msg를 사용하고 있지 않지만 갖고 있음
      • 번들링
        • 여러 파일을 하나로 묶어줌
        • 코드를 압축함
          • min.js
          • 원래 변수가 무엇을 의미하는지 알아야 하니까 map파일이 존재함
  • 타입이 인식하는 원시타입의 종료

    • JS의 7가지 원시타입과 동일
    • 타입스크립트는 자바스크립트를 포함하는 슈퍼셋(상위확장)이므로 자바스크립트가 지원하는 타입을 그대로 사용!!
  • 구문 오류
    - 타입스크립트가 코드로 이해할 수 없는 잘못된 구문을 감지할 때 발생

유니언과 리터럴

타입별칭

  • 타입을 계속 string | boolean을 주기에 번거로우니까 자주 쓰는 타입을 별칭을 만듦
type SBN = string | boolean | number;
  • interface와 차이점
    - 타입별칭
    - 타입을 계속 복사해서 넣어줌
    - 중간에 끼워넣을 수 없음 => 재정의 불가능
    • 인터페이스
      • 클래스처럼 동작함 -> 한번 만들고 계속 참조함
        • 중간에 끼워넣기 가능
          - 복사로 동작하는 것이 아니기 때문에!!!!
        			interface User {id: number; name: string};
        			interface User {addr: string};
        			```
        

유니언 |

  • 타입 확장
  • 합집합이 아니라 또는으로 해석하기

리터럴

  • 원시타입보다 더 구체적인 원시타입
  • 원시값 자체가 타입
  • number와 bigint는 다름!!! => number | bigint
let n: number;
n = 123n; // 에러 발생
  • type narrowing과 type guard
type Member = {
  name: string;
  addr: string;
  discountRate: number;
  t: "Member"; // 타입을 명시
};

type Guest = {
  name: string;
  t: "Guest";
};

const member: Member = {
  name: "홍길동",
  addr: "용산구",
  discountRate: 0.1,
  t: "Member",
};
const guest: Guest = {
  name: "김길동",
  t: "Guest",
};

const who = Math.random() * 0.5 ? member : guest;
// who.discountRate 그냥 접근하면 Guest일 수 있기 때문에 에러 발생
if (who.t === "Member") {
  console.log(who.discountRate);
}
  1. 값 할당을 통한 내로잉 (x=1) assignable infer the value type
    ex) who에 그냥 멤버 타입을 아예 초기화하면 who.discountRate로 접근해도 에러가 발생하지 않음
  2. typeof 검사를 통한 내로잉
  3. 조건 검사를 통한 내로잉 (if x === 'stringValue')
  4. in (cf. hasOwnProperty는 x)
    • 타입스크립트는 hasOwnProperty를 모름
    • 실행할 수 없기 때문에 타입가드 불가능
  5. instanceof
  6. Array.isArray
    static은 runtime에 조작 안되므로 허용

객체 타입

freshness

  • 구조적으로 타입 호환성이 있는 객체 리터럴의 타입 검사를 쉽게 할 수 있도록 신선도라는 개념을 제공
    - 엄격한 객체 리터럴 검사라고 부르기도 함

    • 구조적 타입 시스템
      • 두가지 타입을 비교할 때 타입의 이름이 아니라 구조(속성, 메서드) 기준으로 호환성 결정
        • 유연한 코드 재사용
        • 자바스크립트와의 자연스러운 통합
        • 유연성을 제공하지만 오타/오류를 방지하기 위해 신선도 검사가 추가적으로 적용
  • 왜 필요한가?
    - 구조적 타입 처리는 무언가 실제 다루는 것보다 더 많은 데이터를 받아들인다는 오해를 불러일으킴

    • 따라서 변수에 할당하지 않고 바로 함수에 객체 리터럴로 전달할 때 오타나 잘못된 속성 사용으로 인한 버그를 잡기 위해 필요함!!!
    • 함수 정의에 없는 속성은 객체 리터럴에 없다고 경고하기!
  • 왜 리터럴로 직접 전달했을 때만?
    - 변수로 전달했을 때는 이미 한번 신선도 검사를 통과했거나, 별도의 변수이므로 유연한 구조적 타입 검사만 진행

    • 객체 리터럴로 직접 전달했을 때는 오타나 오해로 간주해서 경고

      			function logName(something: { name: string }) {
      console.log(something.name);
      }
      
      // 'name' 속성을 가지고 있으므로 호환됨!
      var person = { name: 'matt', job: 'being awesome' }; 
      logName(person); // 호환
      			```
      
      ```ts
      			function logName(something: { name: string }) { /* ... */ }
      
      // 신선도 검사에 걸림!
      // logName은 'name'만 필요로 하는데, 'job'이라는 불필요한 속성이 객체 리터럴에 추가되어 있습니다.
      logName({ name: 'matt', job: 'being awesome' });
      
      // 신선도 검사에 걸림!
      // logIfHasName은 'name'을 기대하는데, 'neme'이라는 오타를 사용했습니다.
      logIfHasName({neme: 'I just misspelled name to neme'});
      			```
      
    • 만약 의도적으로 추가한 속성을 받아들이고 싶다면 인덱스 서명 사용

      			// 'foo' 속성 외에도, 어떤 문자열 이름(string)이든 속성으로 가질 수 있음을 명시함
      var x: { foo: number, [key: string]: unknown }; 
      
      x = { foo: 1, baz: 2 }; // 'baz'는 [key: string]에 의해 허용됨
      			```
      
    • React State에서의 활용 (Optional 멤버)

      interface State {
          foo?: string; // 모든 속성을 선택적(Optional)으로 정의
          bar?: string;
      }
      
      // 필요한 속성만 업데이트하는 것은 OK
      this.setState({ foo: "Hello" }); // 구조적 타입 검사 통과
      
      // 오타 입력은 방지됨!
      this.setState({ foos: "Hello" }); // 신선도 검사에서 'foos'는 State에 없다고 거부
      		- Freshness 끄는 방법
      • 변수 할당

      • 타입 캐스팅

        hong = {id: 1, name: 'Hong', addr: 'Pusan'} as TUser;
      • 유니언으로 체크에서 제외시키기

타입체크

  • 공변성 Covariance

    • 타입의 계층 구조가 타입을 포함하는 구조체의 계층 구조와 같은 방향으로 움직이는 성질
    • 코드를 유연하게 작성할 수 있음
      • Animal 목록을 처리하는 함수에 Dog 목록을 전달할 수 있음
    • 함수의 반환 타입은 일반적으로 공변적
      • Animal = Dog 가능
      • Dog = Animal 불가능
      • 왼쪽이 크거나 같아야함!!!!
        따라서 오른쪽이 함수가 오기 때문에 함수의 반환 타입은 작거나 같아야 함
    // 실제 Dog를 반환하는 함수
    const createDog: DogProducer = () => ({ species: "Canis familiaris", name: "Max" });
    
    // 공변성 덕분에 타입 체크 통과
    // DogProducer (자식 반환 함수)를 AnimalProducer (부모 반환 함수)에 할당
    const producer: AnimalProducer = createDog; 
    
    // producer Animal을 반환 예상
    // 실제로는 Dog를 반환하는 createDog가 할당
    // Dog는 Animal의 서브타입이므로, Animal을 기대하는 곳에서 Dog를 받더라도 안전
    
    const result: Animal = producer(); // result는 Animal 타입으로 추론되지만, 실제 값은 Dog입니다.
    console.log(result.species); // "Canis familiaris"
    // console.log(result.name); // 오류: 'Animal' 타입에는 'name' 속성이 없습니다. (타입 안전성 유지)
    
    /*
    // 만약 반대로 할당을 시도하면?
    const createAnimal: AnimalProducer = () => ({ species: "Generic Animal" });
    const invalidProducer: DogProducer = createAnimal; 
    // 오류: 'AnimalProducer' 타입은 'DogProducer' 타입에 할당될 수 없습니다.
    // 왜냐하면 Dog를 기대하는 곳(DogProducer)에 Animal을 반환하는 함수(AnimalProducer)를 넣으면,
    // 반환된 Animal에 name 속성이 없을 수 있어 타입 안전성이 깨지기 때문입니다.
    */
개념설명타입 관계 방향사용되는 곳 (TypeScript 기준)
공변성 (Covariance)서브타입 관계가 같은 방향으로 유지됨.STF<S>F<T>S \subseteq T \Rightarrow F<S> \subseteq F<T>함수의 반환 타입
반공변성 (Contravariance)서브타입 관계가 반대 방향으로 뒤집힘.STF<T>F<S>S \subseteq T \Rightarrow F<T> \subseteq F<S>함수의 매개변수 타입
불변성 (Invariance)서브타입 관계가 유지되지 않음. 두 타입이 정확히 일치해야 호환됨.FFFF는 서로 호환되지 않음대부분의 가변적인 컬렉션 (안전성 확보)
type TUser = { id: number; name: string };
const kim = { id: 2, name: "Kim", addr: "Pusan" };
const y1: TUser[] = [{ id: 1, name: "Hong", addr: "Seoul" }, kim];
const y2: [TUser, TUser] = [{ id: 1, name: "Hong", addr: "Seoul" }, kim];
  • 직접 할당할때는 발생하지만, 이미 정의된 변수를 할당하여 배열 요소로 사용될 때는 잉여 속성 검사가 적용되지 않음 → 구조적 타이핑만 확인
    • 필수 속성을 모두 가지므로 할당 가능하다고 간주
    • but 튜플 타입은 고정된 개수와 순서의 타입을 지정함 → 엄격하게 잉여 속성 검사가 작동

const y1: TUser[] = [{ id: 1, name: "Hong", addr: "Seoul" }];

  • 객체 리터럴이 첫번째 요소로 즉시 할당 → 잉여 속성 포함 → 오류

함수 타입

  • 재귀함수는 return type 명시하기
    • return f(2) + f(1) => f(2)가 숫자인지 문자열인지 알 수 없음
  • 없음을 나타내는 타입
    • null
    • undefined
    • never
      - string & number => 존재하지 않기 때문에 never랑 똑같이 동작
    • void
  • return 타입은 추론으로 하는게 더 안전하다!! 명시하지 말자!!!!

this

  • 값으로 쓰이는 this는 자바스크립트의 this 키워드
  • 타입으로 쓰이는 this는 “다형성 this”
    • 서브클래스의 메서드 체인을 구현할 때 유용
f(this: {x: number}, a: number){} // number를 binding함
f(this: void){} // 바인딩을 막겠다!!!

배열과 튜플

배열 멤버

배열의 멤버가 몇개인지는 타입스크립트는 모름

const arr = [1,2,3];
console.log(arr[0]?.toFixed(1), arr[1] + 100); // arr[1]이 undefined라고 에러

인덱스 시그니처

  • 객체의 속성 이름이 정해져있지 않고 동적일때 속성의 타입과 값의 타입을 미리 정의하는 문법
interface MyDictionary {
	[key: string]: number;
}
interface ScoreList {
    [index: number]: number; // 숫자 key를 가지며, 값은 number 타입
    length: number; // length와 같은 명시적 속성과 공존 가능
}

const scores: ScoreList = {
    0: 90,
    1: 85,
    2: 92,
    length: 3
};

스프레드 연산자

  • concat: 두 배열의 타입이 같아야 함
  • spread: 유니언으로 결합됨
    const result: (string | number)[] = [...result1, ...strings1];

freshness

type MyTuple = [string, number];

// 객체 리터럴: 'extra'는 잉여 속성으로 오류 발생 (Freshness 검사)
// const obj: { name: string } = { name: "A", extra: 1 }; 

// 배열 리터럴: 길이가 다르거나 요소 타입이 달라도 바로 오류가 나지만
// 잉여 속성으로 인한 특별한 "Freshness" 오류가 발생하지 않음
const arr1: MyTuple = ["hello", 42]; // OK
// const arr2: MyTuple = ["hello", 42, "extra"]; // 오류: Source has 3 elements but target allows only 2
type TUser = {id: number, name: string};
const obj = {id: 1, name: 'Lee', addr: 'Seoul', age: 33};
const kim = {id: 2, name: 'Kim', addr: 'Jeju', age:22};
const users3: TUser[] = [
	obj, {id: 5, name: 'Park', age: 33}, kim
]; // 'age'는 TUser에 없음
  • obj, kim 변수 사용 → id, name 속성 갖고 있는지 구조적 타입 검사만 수행
    • 추가 속성을 갖고 있지만 TUser 요구사항 충족하므로 ㄱㅊㄱㅊ ⇒ 구조적 타입 시스템의 유연성
  • {id: 5, name: 'Park', age: 33}
    • 객체 리터럴 → 신선도 검사 진행
    • age 속성이 잉여 속성으로 포함되어있으므로 오류 발생
    • {id: 5, name: 'Park', addr: ‘’, age: 33} → 오류 발생 ㄴㄴ
      • 타입스크립트의 신선도 검사는 객체 리터럴의 모든 속성이 어떤 정의된 속성과 호환되는 한 잉여 속성 오류를 발생시키지 않을 수 있음
      • obj, kim이 구조적 타입 검사를 통과 → 변수에 할당된 객체에는 적용되지 않고 구조적 타입 검사만 적용

튜플

길이와 각 인덱스 요소의 타입이 고정된 배열

특징튜플 (Tuple)배열 (Array)
길이고정 (Fixed)가변 (Variable)
요소 타입인덱스별로 서로 다른 타입 허용모든 요소가 동일한 타입이어야 함
타입 안정성매우 높음 (위치 기반 타입 보장)보통 (요소 타입만 보장)
주요 용도데이터 레코드, 고정된 순서의 값동적 목록, 컬렉션

const assertion

  • 변수에는 as const 사용할 수 없음
  • 리터럴에만 적용 가능

enum

const enum Score{
	A,
	B,
	C
}
  • enum을 배열처럼 사용하면 추후에 변동되면 오류 발생 가능성
    • Score[2]가 아니라 Score.B 같은 형태를 사용하기
  • enum을 쓸때는 꼭 const를 붙이기

interface

  • 확장하고 싶을 때 사용 (오픈소스에 추가할 때)
    • 보통은 타입 별칭 사용

클래스 확장

// Teacher는 StudentTeacher 하위 클래스의 인스턴스에서 사용할 수 있는 teach 메서드를 선언
class Teacher {
    teach() {
        console.log('teaching!');
    }
}

class StudentTeacher extends Teacher {
    learn(){
        console.log('Learning!');
    }
}
const teacher = new StudentTeacher();

teacher.teach() // OK (기본 클래스에 정의됨)
teacher.learn() // OK (하위 클래스에 정의됨)
const t: StudentTeacher = new Teacher(); // Fail
const teacher: Teacher = new StudentTeacher(); // OK (Covariance)
  • extends

    1. interface를 확장하고 싶을 경우

      인터페이스가 다른 인터페이스의 모든 멤버를 상속받을 수 있음

    2. 제네릭 제약

      제네릭 타입을 사용할 때, extends를 사용하여 특정 타입의 서브타입만을 허용하는 제약 조건을 추가할 수 있음

    3. 조건부 타입 설정

      입력 타입에 따라 다른 타입을 변환할 수 있게 한다

      type Check<T> = T extends string ? "String" : "Not String";
      
      type Type1 = Check<string>; // "String"
      type Type2 = Check<number>; // "Not String"
  • as k extends K

    • as
      • 키 재매핑 연산자
      • in을 통해 얻은 키 k를 as 뒤에 오는 타입 표현식의 결과로 반환하거나 필터링

할당 가능성 확장

class PastGrades {
grades : number[] = [];
}

class LabeledPastGrades extends PastGrades {
label? : string; // 없어도 가능?! ⇒⇒ 부모와 동일구조
}
  • 더 추가될 가능성이 있으면 가능, 아니면 그냥 부모에 같이 타입추가

재정의된 생성자

class GradeTally {
grades : number[] = [];
addGrades(...grades : number[]){
this.grades.push(...grades);
return this.grades.length;
}
}

class ContinuedGradesTally extends GradeTally{
	constructor(previousGrades : number[]){
	this.grades = [...previousGrades];
	//Error : 'super' must be called before accessing 'this' in the constructor of a derived class.
	//하위 클래스의 생성자는 this 또는 super에 접근하기 전에 반드시 기본 클래스의 생성자(super())를 호출해야함!
	//super()를 호출하기 전에 this 또는 super에 접근하려고 하는 경우 타입 오류를 보고함
	 
	super.grades; // Fail 
	super();
	console.log('Starting with length', this.grades.length);
	}
}
  • 부모의 생성자가 생겨야 this 사용 가능
  • super()를 무조건 항상 맨 위에 호출하기

재정의된 메서드

  • 내가 계속 확장될 수 있기 때문에 is: boolean 에러남
class GradeCounter {
    countGrades(grades : string[], letter : string) {   // (x: string[], y:string) => 	
        return grades.filter(grade => grade === letter).length;
    }
}

// 기본(super)의 GradeCounter의 반환 타입과 매개변수가 작기 때문에 허용
// ex) x:부모타입 = new 자식() 했을 때 x.f(x, y)와 같이 부모 함수 구조로 요구하므로 자식이 더 많은 param이면 누락되는 arg가 있어 오류!
class FailureCounter extends GradeCounter {
    // countGrades() {  // 모두 OK(:작기 때문에)
    countGrades(grades : string[], _letter: string, is: boolean) { // 에러 발생하게됨
        return super.countGrades(grades, 'F');
    }
}
class GradeCounter {
		is: boolean = false;
		setIs(is: boolean) {this.is = is;}
    countGrades(grades : string[], letter : string) {   // (x: string[], y:string) => 	
        return grades.filter(grade => grade === letter).length;
    }
}

// 기본(super)의 GradeCounter의 반환 타입과 매개변수가 작기 때문에 허용
// ex) x:부모타입 = new 자식() 했을 때 x.f(x, y)와 같이 부모 함수 구조로 요구하므로 자식이 더 많은 param이면 누락되는 arg가 있어 오류!
class FailureCounter extends GradeCounter {
    // countGrades() {  // 모두 OK(:작기 때문에)
    countGrades(grades : string[], _letter: string) {
        return super.countGrades(grades, this.is ? 'F' : 'D');
    }
}

const x: GradeCounter = new FailureCounter();
x.setIs(false);
x.countGrades(['A'], '');

+) 제미나이로 보는 추가 설명

    class GradeCounter {
        countGrades(grades : string[], letter : string) { // 매개변수 2개: (string[], string)
            // ...
        }
    }
    
    class FailureCounter extends GradeCounter {
        countGrades(grades : string[], _letter: string, is: boolean) { // 매개변수 3개: (string[], string, boolean)
            return super.countGrades(grades, 'F');
        }
    }
    const counter: GradeCounter = new FailureCounter();
    counter.countGrades(['A', 'B'], 'C'); // (1)
  • counter.countGrades 를 호출할 때 타스는 매개 변수 2개를 기대하고 인수를 전달함
    • 하지만 실제 객체는 FailureCounter, 매개변수 3개를 요구

    • 런타임 오류 또는 예상치 못한 동작 유발

    • is: booleanis 속성 충돌
      - 부모에 is라는 속성이 있다면 이름 충돌 가능
      - this.is를 참조할 때 혼란을 막기 위해 타스가 경고하거나 오류 발생

      성공 사례

      class GradeCounter {
          is: boolean = false; // 1. is 속성 추가
          setIs(is: boolean) {this.is = is;}
          countGrades(grades : string[], letter : string) { // 매개변수 2개
              // ...
          }
      }
      
      class FailureCounter extends GradeCounter {
          countGrades(grades : string[], _letter: string) { // 매개변수 2개
              return super.countGrades(grades, this.is ? 'F' : 'D'); // 2. is 속성 사용
          }
      }
    • 부모와 동일하게 2개의 매개변수를 가짐

      • LSP 관련 에러 발생하지 않음
    • 부모 클래스로부터 상속받은 is 속성값을 안전하게 사용

      항목부모 메서드 vs. 자식 메서드 (재정의)원칙 및 이유
      매개변수자식은 부모보다 더 적은(또는 같은) 수의 필수 매개변수를 가져야 합니다.부모 타입을 기대하는 호출이 자식 메서드에서 안전하게 작동해야 합니다.
      반환 타입자식의 반환 타입은 부모 반환 타입의 서브 타입이어야 합니다.부모 타입을 기대하는 코드에서 자식의 반환 값을 안전하게 사용할 수 있어야 합니다.
    • 💡 메서드를 재정의(Override)할 때, 자식 클래스의 메서드는 부모 클래스의 메서드보다 매개변수를 더 적게 또는 같게 정의해야 함!!

    • 💡 SOLID
      - OCP: 열려있어야 해 → 객체지향에는 다형성이 제일 중요

    • LSP

      • 부모에서 사용하지 않는데 왜 부모에서 정의해야해요??
        => 자주 사용될 수 있기 떄문에 부모에서 정의하기
    • 만약에 하나의 자식에만 사용한다면…??????

    • 타입스크립트는 변경에 열려있다…!
      - java, c++같은 경우에는 부모에 set을 사용하면 안됨
      - 타입캐스팅을 사용함
      - but 타입스크립트는 타입캐스팅이 없음
      - if (x instanceof FailureCounter)
      - 하려고 한다면 이렇게 if문을 사용해야 함


    • 자바스크립트는 런타임, 타입스크립트는 컴파일될 때를 고려함

      • new FailureCounter() → 실행단계에서 따라서 타입스크립트는 모름
      • 따라서 오로지 x는 GradeCounter 타입

멤버 접근성

  • 타입스크립트 멤버 접근성은 개발할 때만!!! 적용
    • 이러한 키워드는 순수하게 타입 시스템 내에서만 존재함.
    • 자바스크립트로 컴파일 되면 다른 모든 타입 시스템 구문과 함께 키워드도 제거됨!
    • 따라서, 자바스크립트에서 #(private)은 타입스크립트의 private과는 다름!
  • Coding상으론 차이가 없으므로 # 대신 private을 사용해도 괜찮다!
  • 암묵적 public
    isPublicImplicit = 0;
    public isPublicExplicit = 1;
  • 접근자를 먼저 작성하고 readonly 작성하기
  • 생성자에서 멤버 접근성 정의 시 멤버 선언 생략 가능
    class User {
       ~~id; // 안써도 됨
       name; // 안써도됨~~
       private constructor(public id: number, protected name: string) {
           this.id = id;
           this.name = name;
       }
    }
  • 상속에서 this를 return하는 함수의 반환 타입
    class Collection {
       constructor(protected arr: unknown[]) {
           this.arr = arr;
       }
       // push<T>(t: T): Collection { // Bad -> 항상 컬렉션을 반환해야 함
       push<T>(t: T): this {   // Good!
           this.arr.push(t);
           return this;
       }   // Builder Pattern
       print(n: number) { 
           console.log("Collection", n);
       }
    }
    
    class Stack extends Collection {
       print() { 
           console.log("Stack", n);
       }
    }
    • 부모에도 있고 자식에도 있으면 this를 반환해야 실행하는 곳에 맞게 타입을 사용하게 됨!!!

타입 제한자

top 타입 (유니버셜 타입이라고도 함)

  • any, unknown
    • any: 타입 체크 안함
    • unknown: 몰라!
      • (.property) 바로 쓸 수 없음
      • as unknown as string
        • unknown은 모두 가능하기 때문에 as string에서는 에러나도 거쳐가면 에러 없음
      • 에러같은 경우 unknown 사용

타입 서술어 주의사항

const isString = (value: unknown): value is string => 
  typeof value === 'string' && value.length >= 7;

const f1 = (value: string | boolean) => {
  if (isString(value)) {
    console.log(value.toUpperCase());
  } else {
    console.log(value?.length)
  }
};
  • 그럼 6자리이하는 어떡하게..?

typeof

const customer1 = {
  name: 'limeunha',
  mobile: '01051262051',
};

let customer2: typeof customer1; // { name: string, mobile: string }
  • 중복해서 사용학 된다면 type Cust = typeof customer1; 정의하자!
  • 초기값을 설정해서 사용하면 편리함
    const initialCustomer = {
      name: '',
      mobile: '',
    };

keyof

  • 꼭 뒤에 타입이 나와야함!!!!
  • const custKey = keyof typeof initialCustomer;
  • 하나만 바꾸면되어서 추후 유지보수성도 좋음

타입 어서션

  • as
  • satisfies
    const obj = {
      x: 'aaa',
      y: 'sss',
      z: 'zzz' // 에러
    } satisfies { [k in 'x' | 'y']: typeof obj[k] };
    type C = {
    	[k in keyof typeof obj]: string | number; // 문제발생 모두 적용
    }
    type C = {
    	[k in keyof typeof obj]: typeof initValue[k]; // 각각 알맞게 적용
    }

extends keyof

const map = new Map<string, number | string>([['id', 1], ['name', 'Hong']]);
type MapValue<M> = <Map의 value type을 구하는 타입>
type XX6 = MapValue<typeof map>; // number | string
// 정답) type MapValue<M> = M extends Map<unknown, infer Val> ? Val : unknown;
  • 어떤 타입이든 받을 수 있도록 제네릭 매개변수 M
    • map의 타입인 Map<string, number | string>이 들어갈 예정
  • key는 타입 무시, infer로 Val을 추론하기
    • M이 Map이면 Val 반환
    • M이 Map이 아니면 unknown 반환
    • key가 string이 아닌 이유
      • 지금은 map이 string으로 고정되어있지만, map 변수가 아닌 다른 변수가 들어와도 동작하도록 해야 함
      • key의 타입이 중요하지 않음

연습문제

  • 특정 함수의 인자 타입을 추출하는 유틸리티 타입을 작성하시오. (infer)
// type FirstArgs<F> = F extends (...arg: infer ARG) => unknown ? ARG[0] : unknown;
type FirstArgs<F> = F extends (...arg: infer ARG) => void ? ARG[0] : never;
// type FirstArgs<F extends Function> = F extends (
//   a: infer First,
//   ...args: any[]
// ) => void
//   ? First
//   : never;
type SecondArgs<F> = F extends (...arg: infer ARG) => void ? ARG[1] : never;
// type Args<F> = F extends (...arg: infer ARG) => unknown
//   ? ARG extends Array<infer A>
//     ? A
//     : never
//   : unknown;

⇒ 반환하지 않을 때는 unknown이 아니라 void를 사용하자!!!

Type Challenges

T에서 K 프로퍼티만 선택해 새로운 오브젝트 타입을 만드는 내장 제네릭 Pick<T, K>을 이를 사용하지 않고 구현하세요.

interface Todo {
  title: string
  description: string
  completed: boolean
}

type MyPick<T, K> = {
    [k in keyof T as k extends K ? k : never]: T[k] 
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}
  • 더 간결하게 작성해보면

    type MyPick<T, K extends keyof T> = {
        [k in  K]: T[k] 
    }

T의 모든 프로퍼티를 읽기 전용(재할당 불가)으로 바꾸는 내장 제네릭 Readonly를 이를 사용하지 않고 구현하세요.

interface Todo {
  title: string
  description: string
}

type MyReadonly<T> ={
    readonly [k in keyof T] : T[k]
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
  • Readonly
    • 읽기 전용 지정
    • const와의 차이점
      • const: 변수를 참조하기 위한 것
      • readonly: 속성을 위한 것. 속성을 앨리어싱을 통해 변경될 수 있음

배열(튜플)을 받아, 각 원소의 값을 key/value로 갖는 오브젝트 타입을 반환하는 타입을 구현하세요.

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type TupleToObject<T extends readonly PropertyKey[]> = {
    [k in T[number]] : k
}

type result = TupleToObject<typeof tuple> 
// expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
  • T[number]
    • 인덱스 접근 타입
    • 모든 가능한 숫자 인덱스로 접근하여 얻을 수 있는 모든 요소 타입들의 유니온을 추출

회고

저번에 SOPT에서 역대 웨비들 모아두고 진행한 웨비콘 컨퍼런스에 갔을때, 연사분들이 타입스크립트 공부하고 싶으면 타입챌린지를 풀라고 했는데, 이번에 연습문제를 풀면서 타입챌린지랑 비슷한 부분들이 많았다.
그런데 은근(?) 재밌는 것 같기도...ㅎㅎㅎㅎ 다른 사람들이 PR 올려놓은거 보면서 참고하는것도 꽤 도움이 많이 되는 것 같다.

이번 타입스크립트는 궁금했던 부분들을 해소할 수 있어서 좋았다.

Props는 type? inteface?

기존에 프로젝트를 할 때는 Props를 정의할 때 주로 interface를 사용했다. 그런데 강사님이 "interface는 보통 외부 라이브러리나 다른 모듈에서 재정의(Declaration Merging)가 가능해야 하는 API 정의"에 더 적합하다고 말씀하셨다.

interface: 확장에 열려 있다. 동일한 이름으로 다시 선언하면 합쳐지기 때문에, 라이브러리를 만드는 입장에서는 사용자가 기능을 추가할 수 있도록 interface를 제공하는 것이 좋다.

type: 확장에 엄격하다. 한 번 정의하면 덮어쓰거나 합칠 수 없다. 리액트 Props처럼 "이 컴포넌트는 딱 이 데이터만 받아야 해!"라고 명확하게 규정해야 하는 상황에서는 type을 쓰는 것이 의도치 않은 병합을 막아주어 더 안전하다.

결국 "내부적으로 사용하는 Props는 type을, 외부와 소통하는 큰 단위의 규격은 interface를" 사용하는 방향으로 기준을 잡으면 좋을 것 같다.

js랑 ts를 공부하면서 궁금한점이 생겼다.

Object.freeze vs as const

React query에서 쿼리키를 관리할 때, as const를 사용하긴 했는데,
평소에 js로 작성할 때 freeze로 동결시킨적도 있었어서 어떤것이 맞는건지 갑자기 의문이 들어서 강사님한테 여쭤봤다.

질문의 답은 '보장하려는 시점'에 있었다.

as const (TS): 컴파일 타임의 안전성을 위한 도구. 개발자가 코드를 짤 때 실수로 값을 바꾸지 못하도록 IDE 수준에서 빨간 줄을 그어준다. 하지만 실제 JS로 변환되면 일반 객체가 되어 런타임에서 수정이 가능하다.

ex) 코딩할 때부터 막기 위해 as const를 사용하는 것을 지향하기

    function dcRate(cate) {
    	return {dcRate: 0.1, until: '12/30'} as const
    }

Object.freeze (JS): 런타임의 안전성을 위한 도구. 실제 코드가 돌아갈 때 객체를 꽁꽁 얼려버려서, 누군가 값을 수정하려고 하면 에러를 뱉거나 무시한다.

ex) dcRate를 혹시나 개발자가 까먹고 수정할까봐

```tsx
function dcRate(cate) {
	return Object.freeze({dcRate: 0.1, until: '12/30'})
}
```
  

⇒ 우리는 이미 쿼리키를 정했고 규칙을 따르니까 freeze가 아니라 as const가 더 알맞다!!
⇒ JS보다 TS가 더 유리하다!!

그러면 언제 freeze를 쓰는걸까...??? 또 궁금해져서 여쭤보니 외부에 npm 같은 곳에 라이브러리를 공유할 때는 freeze로 막아주는 것이 안전하다고 하셨다.
평소에 개발할 때는 as const를 사용하도록 하자!!

profile
또이의 개발새발 개발일기

0개의 댓글