Closed moonheekim0118 closed 2 years ago
타입스크립트에서의 타입은 ‘할당 가능한 값들의 집합' 이다
타입스크립트에서 never 타입은 공집합
유닛타입 = 리터럴 타입
type A = 'A';
유니온 타입
type AB = 'A' | 'B'
type AB12 = 'A' | 'B' | 12;
type AB = keyof (AB | AB12) -> never
인터섹션 타입
interface Person {
name: string;
}
interface Lifespan {
birth : Date;
death ?: Date;
}
type PersonSpan = Person & LifeSpan;
-> name, birth, death 모두 사용 가능하다
서브 타입
interface Person{
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death ?: Date;
}
-> PersonSpan의 값들은 name과 birth 속성을 가져야 제대로된 서브집합이라고 볼 수 있다
unknown
*일단 할 게 많아서 급한 불 좀 끄고 다시 와서 정리 하겠습니당
가장 작은 집합은 공집합이며, 타입스크립트에서는 never 타입이다.
그 다음으로 작은 집합은 한 가지 값만 포함하는 타입이다. 타입스크립트에서는 리터럴(literal) 타입이다.
두 개, 새 개로 묶으려면 유니온(union) 타입을 사용한다. 유니온 타입은 값 집합들의 합집합이다.
타입스크립트 오류 중 ‘할당 가능한'아라는 문구는 ~의 원소 또는 ~의 부분 집합 관계를 의미한다.
집합의 관점에서, 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이다.
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan
& 연산자은 두 타입의 교집합을 계산한다. 결과값이 공집합이 나올 것으로 예상할 수 있지만 타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용된다. 그래서 Person과 Lifespan을 둘 다 가지는 값이 인터섹션 타입에 속하게 된다. 인터섹선 타입의 값은 각 타입 내의 속성을 모두 포함하는 것이 일반적인 규칙이다.
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)
타입이 집합이라는 관점에서 extends의 의미는 ~의 부분 집합이라는 의미로 받아들일 수 있다.
function getKey<K extends string>(val: any, key: K) {
}
K는 string의 부분 집합 범위를 가지는 어떠한 타입. string 리터럴 타입, string 리터럴 타입의 유니온, string 자신을 포함한다.
interface Person {name: string};
const alice: Person = {name: 'Alice'};
const bob = {name: 'Bob'} as Person;
두 가지 방법은 결과가 같아 보이지만 그렇지 않다. 첫번째 방법은 타입 선언, 두번째 방법은 타입 단언이다. 타입 단언은 타입스크립트가 추론한 타입이 있더라도 Person 타입으로 간주한다. 타입 단언보다 타입 선언을 사용하는게 낫다.
타입 선언은 할당되는 값이 해당 인터페이스를 만족하는지 검사한다. 타입 단언은 강제로 타입을 지정했으니 타입 체커에게 오류를 무시하라고 하는 것이다. 타입 단언이 꼭 필요한 경우가 아니라면, 안전성 체크가 되는 타입 선언을 사용하는 것이 좋다.
const bob = <Person>{}
은 {} as Person과 동일하다. 이런 형식은 리액트에서 컴포넌트 태그로 인식되기 때문에 잘 쓰이지 않는다.
interface Person {name: string};
const people = ['alice', 'bob', 'jan'].map(name => ({name})); // Person[]을 원했지만 결과는 {name: string;}[]
const people2 = ['alice', 'bob', 'jan'].map(name => ({name} as Person)); // 타입 단언
const people3 = ['alice', 'bob', 'jan'].map(name => {
const person: Person = {name};
return person
}); // 타입 선언
const people4 = ['alice', 'bob', 'jan'].map((name): Person => ({name})); // 타입 선언
화살표 함수의 타입 선언은 추론된 타입이 모호할 때가 있다. 타입 단언 형식보다는 타입 선언 형식으로 표현해주자
(name): Person
은 name의 타입이 없고, 반환 타입이 Person이라고 명시한다. (name: Person)
은 name의 타입이 Person임을 명시하고 반환 타입은 없다는 의미이기 때문에 오류가 발생한다.
타입 단언은 타입 체커가 추론한 타입보다 우리가 판단하는 타입이 더 정확할 때 의미가 있다. 모든 타입은 unknown의 서브타입이기 때문에 unknown이 포함된 단언문은 항상 동작한다.
string 기본형에는 메서드가 없지만, 자바스크립트에는 메서드를 가지는 String 객체 타입이 정의되어 있다. 다른 기본형에도 동일하게 객체 래퍼 타입이 존재한다. 이 래퍼 타입들 덕분에 기본형 값에 메서드를 사용할 수 있고, 정적 메서드도 사용할 수 있다. 타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다.
string은 String에 할당 할 수 있지만 String은 string에 할당할 수 없다.
type K = keyof (Person | Lifespan); // 타입이 never
keyof (A|B) = (keyof A)& (keyof B)
keyof (A&B) = (keyof A) | (keyof B)
const people = ['alice', 'bob', 'jan'].map(name => ({name}));
// Person[] 을 원했지만 결과는 { name: string; } []
// 타입 단언을 쓰면
const people = ['alice', 'bob', 'jan'].map(name => ({name} as Person));
// 선언하기 1번
const people = ['alice', 'bob', 'jan'].map(name=> {
const person: Person = {name};
return person;
} ); // 타입은 Person[]
// 선언하기 2번 (리팩터링)
const people = ['alice', 'bob', 'jan'].map((name):Person => ({name}));
// 타입은 Person[]
(name):Person
은 name 의 타입이 없고 반환 타입이 Person 이라고 명시한다. const el = document.body as Person; // 에러
const el = document.body as unknown as Person; // ㅇㅋ
interface Room {
numDoors: number;
ceilingHeightFt: number;
}
const r:Room {
numDoors:1,
ceilingHeightFt:10,
elephant: 'present'
}
// 개체 리터럴은 알려진 속성만 지정할 수 있으며, Room 형식에 elephant 가 없습니다.
interface Option{
title: string;
darkMode?:boolean;
}
const o1 : Option = document;
const o2 : Option = new HTMLAnchorElement;
// 에러 없음
왜 o1. 과 o2 의 경우 에러가 없을까?
일단 잉여속성 체크를 하지 못했고 (객체 리터럴로 값을 할당한게 아니므로), 그에 따라서 document 와 HTMLAnchorElement 에는 string 타입인 title 속성이 있기 때문에!
따라서 위의 할당문은 정상이다.
잉여 속성을 체크함으로써, 기본적으로 타입 시스템의 구조적 본질을 해치지 않으면서 객체 리터럴에 알 수 없는 속성을 허용하지 않아서 앞에서 다룬 문제점들을 해결 할 수 있다.
type BinaryFn = (a: number, b:number)=>number;
const add: BinaryFun =(a,b)=>a+b;
// 인터페이스가 타입 확장
interface IStateWithPop extends TState{
population:number;
}
// 타입이 인터페이스 확장
type TStateWithPop = IState & { population: number;}
type Input = { } ;
type Output = {} ;
inteface VariableMap {
[name:string] : Input | Output;
}
type NamedVariabel = (Input | Output) & { name: string } ;
type Pari = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];
interface IState { name: string; capital: string; }
interface IState{ population:number; }
const newyork : IState{ name: 'NewYork', capital: 'NewYork', population: 500_00 }
- 위와 같은 선언 병합은 주로 타입 선언 파일에서 사용된다.
- 따라서 선언 합을 지원하기 위해서는 무조건 inteface 를 사용해야한다.
- 타입스크립트는 여러 버전의 자바스크립트 표준 라이브러리에서 여러 타입을 모아 병합한다. 병합을 통해서 Array 인터페이스에 여러 메서드가 추가된다.
- 따라서 프로퍼티가 추가되는 것을 원치 않는다면, 인터페이스 대신 타입을 사용해야한다.
-
## 몰랐던 지식
```ts
inteface Example{
[key:string] : string;
}
이거를 인덱스 시그니처라고 한다.
편집기의 타입스크립트 언어 서비스를 적극 활용해야 한다. 언어 서비스는 보통 편집기를 통해서 제공된다. 자동 완성 같은 서비스를 사용하면 코드 작성이 간편해진다. 보통의 경우 심벌 위에 마우스 커서를 대면 타입스크립트가 그 타입을 어떻게 판단하고 있는지 확인할 수 있다.
이를 활용하면 타입 시스템이 어떻게 동작하는지, 타입스크립트가 어떻게 타입을 추론하는지 개념을 잡을 수 있다.
변수에는 다양한 종류의 값을 할당할 수 있다. 타입은 ’할당 가능한 값들의 집합’이고, 이 집합은 타입의 ‘범위’라고 부르기도 한다.
never: 가장 작은 집합. 아무 값도 포함하지 않는 공집합. never 타입으로 선언된 변수에는 아무런 값도 할당할 수 없다.
유닛(unit) 또는 리터럴(literal): 한 가지 값만 포함하는 타입
type A = 'A';
type B = 'B';
type Twelve = 12;
유니온(union): 두 개 이상의 값을 가지는 타입
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
집합의 관점에서 타입 체커의 주요 역할은 하나의 집합이 다른 집합의 부분 집합인지 검사하는 것이다.
타입 연산자는 인터페이스의 속성이 아닌, 값의 집합(타입의 범위)에 적용된다.
& 연산자는 두 타입의 인터섹션(intersection, 교집합)을 계산한다.
아래의 경우 두 인터페이스가 공통으로 가지는 속성이 없으니 아래의 PersonSpan을 공집합(never 타입)으로 예상하기 쉽다.
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
그러나 타입은 ‘할당 가능한 값들의 집합’임을 명심해야한다. Person과 Lifespan의 교집합은 Person 타입이 될 수도 있고, Lifespan 타입이 될 수도 있는 값들의 집합이다. 두 타입 모두에 해당하는 값의 집합이다.
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
} // 정상
예를 들어 string | number
와 string | Date
간의 교집합은 string
이다.
extends 또한 비슷하게 값의 집합 관점에서 해석해보자. 아래의 경우, ‘B의 부분 집합인 A’로 해석할 수 있다.
interface A extends B {
...
}
extends 키워드는 제너릭 타입에서 한정자로 쓰이는데, 이 문맥에서도 ‘~의 부분집합`을 의미한다.
아래의 경우 K는 string의 부분 집합 범위를 가지는 어떤 타입이 된다. 즉 K는 string 리터럴 타입, string 리터럴 타입의 유니온, string 자신이 될 수 있다.
function getKey<K extends string>(val: any, key: K) {
// ...
}
타입이 값의 집합이라는 관점에서 배열과 튜플의 관계가 명확해진다.
구조적 타이핑 관점에서 트리플은 페어에 할당 가능할 것으로 보인다.
그러나 [number, number, number]는 [number, number]에 할당할 수 없다. 타입스크립트는 숫자 쌍에 length 속성을 가지고 있고, 이 length의 값이 맞지 않기 때문에 트리플을 페어에 할당할 수 없다.
typeof, this 그리고 많은 연산자와 키워드들은 타입 공간과 값 공간에서 다른 목적으로 사용될 수 있다.
타입의 관점에서 typeof는 타입 스크립트 타입을 반환한다. 반환된 타입을 이용하여 type 구문으로 이름을 붙이거나, 더 큰 타입의 일부로 사용할 수 있다.
값의 관점에서 typeof는 자바스크립트 런타임의 typeof 연산자다. 대상 심벌의 런타임 타입을 가리키는 문자열을 반환한다.
타입스크립트에서 변수에 값을 할당하고 타입을 부여하는 방법은 두 가지다.
타입 단언(as Type)보다 타입 선언(: Type)을 사용해야 한다.
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // 타입 선언
const bob = { name: 'Bob' } as Person; // 타입 단언
const bob = <Person>{ name: 'Bob' }; // 타입 단언의 원래 문법. tsx 컴포넌트 태그로 인식되어 현재는 잘 쓰이지 않음
타입 단언은 타입 체커가 추론한 타입보다 개발자가 판단하는 타입이 더 정확할 때 의미가 있다.
자바스크립트는 기본형과 객체 타입을 서로 자유롭게 변환한다.
string 기본 형에 charAt 같은 메서드를 사용할 때, 자바스크립트는 기본형을 String 객체로 래핑(wrap)하고, 메서드를 호출하고, 마지막에 래핑한 객체를 버린다.
String.prototype을 몽키-패치(monkey-patch)하면 이 내부 동작을 관찰할 수 있다. 내장 메서드가 호출될 때 this를 출력하도록 몽키-패치하면 메서드 내의 this는 string 기본형이 아닌 String 객체 래퍼임을 확인할 수 있다. 이 래퍼 타입 덕분에 기본형 값에 메서드를 사용할 수 있다.
기본형과 객체 래퍼가 항상 동일하게 동작하는 것은 아니다. 직접 객체 래퍼를 사용하거나 인스턴스를 생성하는 것은 피해야 한다.
타입스크립트는 기본형과 객체 래퍼 타입을 별도로 모델링한다. 객체 래퍼 타입은 지양하고 기본형 타입을 사용해야 한다.
둘을 잘못 사용하지 않도록 주의하자. string(기본형)은 String(객체 래퍼 타입)에 할당할 수 있지만 String은 string에 할당할 수 없다.
interface Options {
title: string;
darkMode?: boolean;
}
const o1: Options = {
title: 'Spider Solitaire';
darkMode: true;
elephant: 'present',
} // 에러! 개체 리터럴은 알려진 속성만 지정할 수 있으며 'Options ' 형식에 'elephant'가 없습니다.
// => 잉여 속성 체크
const o2: Options = { title: 'Ski Free', darkmode: true };
// 'Options' 형식에 'darkmode'이(가) 없습니다.
// => 잉여 속성 체크
const o3: Options = document; // 정상 <= string 타입인 title 속성을 가지기 때문
const o4: Options = new HTMLAnchorElement; // 정상 <= string 타입인 title 속성을 가지기 때문
const intermediate = { title: 'Ski Free', darkmode: true };
const o5: Options = intermediate;
// 정상!
// intermediate이 객체 리터럴이 아니기 때문에 잉여 속성 체크가 적용되지 않는다.
const o6 = { title: 'Ski Free', darkmode: true } as Options;
// 정상!
// 타입 단언문을 사용할 때 잉여 속성 체크가 적용되지 않는다.
타입스크립트는 단순히 런타임의 오류 코드를 잡아내는 것 뿐만 아니라, 의도와 다르게 작성된 코드를 찾으려고 한다. 이를 위한 기능 중 하나가 바로 잉여 속성 체크다.
타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 해당 타입의 속성이 있는지, 그리고 ‘그 외의 속성은 없는지’ 확인한다. 이를 잉여 속성 체크라고 한다.
위의 Options 타입은 범위가 매우 넓기 때문에, 순수한 구조적 타입 체커는 의도와 다르게 작성된 코드와 같은 오류를 찾아내지 못 한다. 이때 잉여 속성 체크를 통해 타입 시스템의 구조적 본질을 해치지 않으면서 알 수 없는 속성을 허용하지 않음으로써 위와 같은 문제를 방지할 수 있다.
잉여 속성 체크는 조건에 맞을 때 동작한다는 한계가 있다.
객체 리터럴을 할당할 때 동작하며, 중간에 임시 변수를 도입하여 변수에 변수를 할당하면 동작하지 않는다.
구조적 타이핑에 기반을 둔 할당 가능 검사와 함께 쓰이나, 잉여 속성 체크와 할당 가능 검사는 별도의 과정이다.
잉여 속성 체크를 원치 않는다면, 인덱스 시그니처를 사용하여 타입스크립트가 추가적인 속성을 예상하도록 할 수 있다.
매개변수나 반환 값에 타입을 명시하기보다 함수 표현식 전체에 타입 구문을 적용하는 것이 좋다. 함수의 매개변수부터 반환값까지 함수 전체를 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문이다.
반복되는 함수의 시그니처를 하나의 함수 타입으로 통합하면, 타입 구문이 적어지고 구현부와 타입을 분리하여 로직이 보다 분명해진다.
다른 함수의 시그니처를 참조하려면 typeof fn을 사용하면 된다.
비도 오고 그래서~ 타스 생각이 났어 ~~