
저번 회고에 이어서 디지털하나로의 2번째 과목(?)인 TS를 회고해보려고 한다.
TS는 JS보다는 조금 짧게 진행했다.
약 7일 동안 배웠고, JS는 메모리랑 동작원리와 함께 진행했는데 지금은 이미 JS로 배웠기 때문에 다루지 않아서 더 짧게 한것 같다.
TS는 사실 제대로 공부하지는 않았고, 프로젝트할 때 사용해보긴한 상태였다.
그런데 타입을 지정하는게 아니라 내가 타입에게 잡아먹히면서 개발을 했다고 생각해서
이번 기회에 바로잡고 가고 싶었다.
이번에 수업 때 배우고, 또 야자하면서 같이 공부한 내용들을 정리하면서 회고를 해보겠다!!!!
*.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);
}
타입이 인식하는 원시타입의 종료
구문 오류
- 타입스크립트가 코드로 이해할 수 없는 잘못된 구문을 감지할 때 발생
string | boolean을 주기에 번거로우니까 자주 쓰는 타입을 별칭을 만듦type SBN = string | boolean | number;
interface User {id: number; name: string};
interface User {addr: string};
```
|또는으로 해석하기number | bigintlet n: number;
n = 123n; // 에러 발생
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);
}
구조적으로 타입 호환성이 있는 객체 리터럴의 타입 검사를 쉽게 할 수 있도록 신선도라는 개념을 제공
- 엄격한 객체 리터럴 검사라고 부르기도 함
왜 필요한가?
- 구조적 타입 처리는 무언가 실제 다루는 것보다 더 많은 데이터를 받아들인다는 오해를 불러일으킴
왜 리터럴로 직접 전달했을 때만?
- 변수로 전달했을 때는 이미 한번 신선도 검사를 통과했거나, 별도의 변수이므로 유연한 구조적 타입 검사만 진행
객체 리터럴로 직접 전달했을 때는 오타나 오해로 간주해서 경고
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
// 실제 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) | 서브타입 관계가 같은 방향으로 유지됨. | 함수의 반환 타입 | |
| 반공변성 (Contravariance) | 서브타입 관계가 반대 방향으로 뒤집힘. | 함수의 매개변수 타입 | |
| 불변성 (Invariance) | 서브타입 관계가 유지되지 않음. 두 타입이 정확히 일치해야 호환됨. | 와 는 서로 호환되지 않음 | 대부분의 가변적인 컬렉션 (안전성 확보) |
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];
const y1: TUser[] = [{ id: 1, name: "Hong", addr: "Seoul" }];
return f(2) + f(1) => f(2)가 숫자인지 문자열인지 알 수 없음string & number => 존재하지 않기 때문에 never랑 똑같이 동작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
};
const result: (string | number)[] = [...result1, ...strings1];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에 없음
{id: 5, name: 'Park', age: 33}{id: 5, name: 'Park', addr: ‘’, age: 33} → 오류 발생 ㄴㄴ길이와 각 인덱스 요소의 타입이 고정된 배열
| 특징 | 튜플 (Tuple) | 배열 (Array) |
|---|---|---|
| 길이 | 고정 (Fixed) | 가변 (Variable) |
| 요소 타입 | 인덱스별로 서로 다른 타입 허용 | 모든 요소가 동일한 타입이어야 함 |
| 타입 안정성 | 매우 높음 (위치 기반 타입 보장) | 보통 (요소 타입만 보장) |
| 주요 용도 | 데이터 레코드, 고정된 순서의 값 | 동적 목록, 컬렉션 |
const enum Score{
A,
B,
C
}
Score[2]가 아니라 Score.B 같은 형태를 사용하기// 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
interface를 확장하고 싶을 경우
인터페이스가 다른 인터페이스의 모든 멤버를 상속받을 수 있음
제네릭 제약
제네릭 타입을 사용할 때, extends를 사용하여 특정 타입의 서브타입만을 허용하는 제약 조건을 추가할 수 있음
조건부 타입 설정
입력 타입에 따라 다른 타입을 변환할 수 있게 한다
type Check<T> = T extends string ? "String" : "Not String";
type Type1 = Check<string>; // "String"
type Type2 = Check<number>; // "Not String"
as k extends K
asclass 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);
}
}
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: boolean과 is 속성 충돌
- 부모에 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개의 매개변수를 가짐
부모 클래스로부터 상속받은 is 속성값을 안전하게 사용
| 항목 | 부모 메서드 vs. 자식 메서드 (재정의) | 원칙 및 이유 |
|---|---|---|
| 매개변수 | 자식은 부모보다 더 적은(또는 같은) 수의 필수 매개변수를 가져야 합니다. | 부모 타입을 기대하는 호출이 자식 메서드에서 안전하게 작동해야 합니다. |
| 반환 타입 | 자식의 반환 타입은 부모 반환 타입의 서브 타입이어야 합니다. | 부모 타입을 기대하는 코드에서 자식의 반환 값을 안전하게 사용할 수 있어야 합니다. |
💡 메서드를 재정의(Override)할 때, 자식 클래스의 메서드는 부모 클래스의 메서드보다 매개변수를 더 적게 또는 같게 정의해야 함!!
💡 SOLID
- OCP: 열려있어야 해 → 객체지향에는 다형성이 제일 중요
LSP
만약에 하나의 자식에만 사용한다면…??????
타입스크립트는 변경에 열려있다…!
- java, c++같은 경우에는 부모에 set을 사용하면 안됨
- 타입캐스팅을 사용함
- but 타입스크립트는 타입캐스팅이 없음
- if (x instanceof FailureCounter)
- 하려고 한다면 이렇게 if문을 사용해야 함
자바스크립트는 런타임, 타입스크립트는 컴파일될 때를 고려함
new FailureCounter() → 실행단계에서 따라서 타입스크립트는 모름isPublicImplicit = 0;
public isPublicExplicit = 1;class User {
~~id; // 안써도 됨
name; // 안써도됨~~
private constructor(public id: number, protected name: string) {
this.id = id;
this.name = name;
}
}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);
}
}.property) 바로 쓸 수 없음as unknown as stringunknown 사용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)
}
};
const customer1 = {
name: 'limeunha',
mobile: '01051262051',
};
let customer2: typeof customer1; // { name: string, mobile: string }
type Cust = typeof customer1; 정의하자!const initialCustomer = {
name: '',
mobile: '',
};const custKey = keyof typeof initialCustomer;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]; // 각각 알맞게 적용
}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;
// 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를 사용하자!!!
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]
}
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
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'}
저번에 SOPT에서 역대 웨비들 모아두고 진행한 웨비콘 컨퍼런스에 갔을때, 연사분들이 타입스크립트 공부하고 싶으면 타입챌린지를 풀라고 했는데, 이번에 연습문제를 풀면서 타입챌린지랑 비슷한 부분들이 많았다.
그런데 은근(?) 재밌는 것 같기도...ㅎㅎㅎㅎ 다른 사람들이 PR 올려놓은거 보면서 참고하는것도 꽤 도움이 많이 되는 것 같다.
이번 타입스크립트는 궁금했던 부분들을 해소할 수 있어서 좋았다.
기존에 프로젝트를 할 때는 Props를 정의할 때 주로 interface를 사용했다. 그런데 강사님이 "interface는 보통 외부 라이브러리나 다른 모듈에서 재정의(Declaration Merging)가 가능해야 하는 API 정의"에 더 적합하다고 말씀하셨다.
interface: 확장에 열려 있다. 동일한 이름으로 다시 선언하면 합쳐지기 때문에, 라이브러리를 만드는 입장에서는 사용자가 기능을 추가할 수 있도록 interface를 제공하는 것이 좋다.
type: 확장에 엄격하다. 한 번 정의하면 덮어쓰거나 합칠 수 없다. 리액트 Props처럼 "이 컴포넌트는 딱 이 데이터만 받아야 해!"라고 명확하게 규정해야 하는 상황에서는 type을 쓰는 것이 의도치 않은 병합을 막아주어 더 안전하다.
결국 "내부적으로 사용하는 Props는 type을, 외부와 소통하는 큰 단위의 규격은 interface를" 사용하는 방향으로 기준을 잡으면 좋을 것 같다.
js랑 ts를 공부하면서 궁금한점이 생겼다.
vs as constReact 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를 사용하도록 하자!!