Closed prefer2 closed 1 year ago
타입스크립트는 정적이면서도 동적인 특성을 동시에 가진다. 따라서 타입스크립트는 프로그램의 일부분에만 타입 시스템을 적용할 수 있다. 마이그레이션을 할 때 코드의 일부분에 타입 체크를 비활성화시켜 주는 any 타입이 중요한 역할을 한다
function processBar(b: Bar) {}
function f() {
const x = returningFoo();
processbar(x); // Foo 형식의 인수는 Bar 형식의 매개변수에 할당할 수 없다
}
// solution 1
function f1() {
const x: any = returningFoo();
processbar(x);
}
// solution 2. 권장되는 방법
function f1() {
const x = returningFoo();
processbar(x as any);
}
두번째 방법인 x as any 형태가 권장된다. any타입이 함수의 매개변수에서만 사용되어 다른 코드에는 영향을 미치지 않기 때문이다. 1번 방법의 경우 호출 이후에도 x가 any타입인 반면, 2번째 방법은 호출 이후에는 x가 그대로 Foo 타입이다.
함수에서 any 타입을 반환하면 그 영향력이 프로젝트 전반으로 퍼지게 된다.
타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에는 함수의 반환타입을 명시하는 것이 좋다.
반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있다.
function f() {
const x = returningFoo();
// @ts-ignore
processbar(x);
}
@ts-ignore를 사용하면 다음 줄의 오류가 무시된다. 하지만 타입 체커가 알려주는 오류는 문제가 될 가능성이 높은 부분들이기 때문에 원인을 찾아 대처하는 것이 옳다.
// 객체 안의 한 개의 속성이 타입 오류를 가지는 경우
const config: Config = {
a = 1,
b = 2,
c ={
key: value // 오류
}
}
// solution 1
const config: Config = {
a = 1,
b = 2,
c ={
key: value
}
} as any
// 객체 전체를 any로 단언하면 다른 속성들(a,b)이 타입 체크가 되지 않는 부작용이 생긴다.
// solution 2
const config: Config = {
a = 1,
b = 2,
c ={
key: value as any
}
}
// 최소한의 범위에서만 any를 사용하자
any는 모든 값을 표현할 수 있는 매우 큰 범위의 타입이다. 일반적인 상황에서는 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높기 때문에 더 구체적인 타입을 찾아 타입 안전성을 높여야 한다.
function getLengthBad(array: any){
return array.length; // any
}
function getLength(array: any[]){
return array.length; // number
}
any보다 any[ ]가 더 좋은 이유
함수의 매개변수를 구체화 할 때 배열의 배열 형태라면 any[ ][ ]처럼 선언하면 된다.
함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[key: string]: any}처럼 선언하면 된다. object 타입을 사용 할 수도 있다. object 타입은 객체의 키를 열거할 수는 있지만, 속성에 접근할 수는 없다(obj[key] 불가능)
객체이지만 속성에 접근할 수 없어야 한다면 unknown타입이 좋을 수도 있다.
함수의 내부로직이 복잡해 모든 부분을 타입으로 구현하는 것이 어렵다면 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확하게 명시하는 것이 좋다.
declare function shallowEqual(a: any, b: any): boolean;
function shallowEqual<T extends object>(a: T, b: T): boolean {
for( const [k, aVal] of Object.entries(a)) {
if(!(k in b) || aVal !== b[k]){ // {} 형식에 인덱스 시그니처가 없어 암시적으로 any 형식이 있다
return false;
}
}
return Object.keys(a).length === Object.keys(b).length
}
// any를 사용해서 오류 해결.
// 실제 오류가 아니라는 것을 알고 있기 때문에 any로 단언 가능
function shallowEqual<T extends object>(a: T, b: T): boolean {
for( const [k, aVal] of Object.entries(a)) {
if(!(k in b) || aVal !== (b as any)[k]){
return false;
}
}
return Object.keys(a).length === Object.keys(b).length
}
타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정되고, 이는 대부분 확장할 수 없다. 하지만 any타입은 예외인 경우가 있다.
function range(start: number, limit: number){
const out = [];
for( let i = start; i < limit; i++ ){
out.push(i);
}
return out; // 반환 타입이 number[]로 추론됨
}
out의 타입은 any[ ]로 선언되었지만 number 타입의 값을 넣는 순간부터 number[ ]로 진화(evolve)한다. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화한다.
const result = []; // any[]
result.push('a'); // string[]
result.push(1); // (string | number)[]
any타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적으로 any인 경우에만 일어난다. 명시적으로 any를 선언하면 타입이 그대로 유지된다.
타입의 진화는 값을 할당하거나 요소를 넣은 후에만 일어나기 때문에 편집기에서는 예상과 다르게 보일 수 있다.
암시적 any 샅애인 변수에 어떠한 할당도 하지 않고 사용하려고 하면 암시적 any 오류가 발생하게 된다.
function range(start: number, limit: number){
const out = [];
if( start === limit) {
return out; // out 변수는 암시적으로 any[] 형식이 포함됩니다.
}
for( let i = start; i < limit; i++ ){
out.push(i);
}
return out;
}
any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다. 그리고 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생한다.
타입을 안전하게 사용하기 위해서는 암시적 any를 진화시키는 것보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계이다.
let variable: unknown
variable = true // OK (boolean)
variable = 1 // OK (number)
variable = 'string' // OK (string)
variable = {} // OK (object)
let variable: unknown
let anyType: any = variable
let booleanType: boolean = variable
// Error: Type 'unknown' is not assignable to type 'boolean'.
let numberType: number = variable
// Error: Type 'unknown' is not assignable to type 'number'.
let stringType: string = variable
// Error: Type 'unknown' is not assignable to type 'string'.
let objectType: object = variable
// Error: Type 'unknown' is not assignable to type 'object'.
function safeParse<T> (s: string): T {
return parsed(s)
}
unknown 대신 제너릭 매개변수를 사용하는 경우도 있다. 하지만 이는 사실상 타입 단언문과 동일하다. 제너릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다.
@ts-ignore
를 사용하여 강제로 타입 오류 제거하기function processBar(b: Bar) { /* */}
function expressionReturningFoo(): Foo { /* */ };
function f() {
const x = expressionReturningFoo();
processBar(x);
// 오류! 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없다.
}
function f1() {
const x: any = expressionReturningFoo();
// f1 범위에서는 x의 타입이 any다. 영향 범위가 크다.
processBar(x);
// 여기서 x를 반환까지 하면? any는 함수 바깥까지 영향을 미치게 된다.
}
function f2() {
const x = expressionReturningFoo();
processBar(x as any);
// 매개변수로 들어갈 때만 any다. 이후에 x는 다시 Foo 타입이다.
}
// any를 사용하지 않고 오류를 제거할 수도 있다.
function f3() {
const x = expressionReturningFoo();
// @ts-ignore
processBar(x);
}
as any
로 선언하기보다 최소한의 범위에만 any 사용하기const config: Config = {
a: 1,
b: 2,
c: {
key: value
}
} as any;
// 객체 전체를 any로 단언하면 다른 속성들도 타입 체크가 안 된다.
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any
}
}
// 최소한의 범위에 any를 사용하여 다른 속성들은 타입 체크가 되도록!
any는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입이다. 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘리먼트는 물론 null과 undefined까지 포함한다. 즉, 일반적으로는 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다. 그러므로 더 구체적인 타입을 찾아 타입 안전성을 높이도록 하자.
아래의 경우 매개변수 타입을 any보다 any[]를 사용하는 것이 좋다.
function getLengthBad(array: any) {
return array.length;
}
// 타입은 function getLengthBad(array: any): any
function getLength(array: any[]) {
return array.length;
}
// 타입은 unction getLength(array: any[]): number
// 반환 타입이 적절하게 추론된다!
객체의 값을 알 수 없는 경우에는 타입을 { [key: string]: any }
로 선언한다.
// object 타입을 사용하면 객체의 키를 열거할 수 있지만 속성에 접근할 수 없다.
function hasTwelveLetterKey1(o: object) {
for (const key in o) {
if (key.length === 12) {
console.log(key, o[key]);
// 오류! '{}' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
}
}
return false;
}
// object의 값을 알 수 없을 때는 인덱스 시그니처를 추가하여 값의 타입을 any로 준다.
function hasTwelveLetterKey2(o: { [key: string]: any }) {
for (const key in o) {
if (key.length === 12) {
return true;
}
}
return false;
}
함수 타입도 any를 사용하되 최소한으로나마 구체화한다.
type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개변수. "Function" 타입과 동일하다.
함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의는 정확히 명시한다. 타입 단언은 타입을 위험하게 만들지만 상황에 따라 현실적인 해결책이 되기도 한다. 불가피하게 사용하는 경우 정확한 정의를 가지는 함수 안으로 숨긴다.
declare function shallowObjectEqual<T extends object>(a: T, b: T): boolean;
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== b[k]) {
// b[k] 오류!
// '{}' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
return false;
}
}
return false;
}
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
if (!(k in b) || aVal !== (b as any)[k]) {
// b에 k 속성이 있다는 것을 확인해고, 실제 오류가 아니라는 것을 알고 있으므로
// any로 단언하여 해결한다.
return false;
}
}
}
noImplicitAny
가 설정된 상태에서 암시적 any
와 any[]
타입은 진화할 수 있다.
명시적으로 any를 선언하면 타입은 그대로 유지된다.
배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되고 진화한다.
function range(start: number, limit: number) {
const out = []; // 타입 any[]
for (let i = start; i < limit; i++) {
out.push(i);
}
return out; // 타입 number[]
}
const result = [];
result.push('a');
result // 타입 string[]
result.push(1);
result // 타입 (string|number)[]
조건문의 분기 또는 try/catch 블록 안에서 변수를 할당하는 경우 any의 진화가 일어난다.
let val; // 타입 any
if (Math.random() < 0.5) {
val = /hello/;
val // 타입 RegExp
} else {
val = 12;
val // 타입 number
}
val // 타입 number | RegExp
let val = null; // 타입 any
try {
somthingDangerous();
val = 12;
val // 타입 number
} catch (e) {
console.warn('alas!');
}
val // 타입 number | null
any를 진화시키는 방법보다 명시적 타입 구문을 사용하는 것이 더 안전한 설계다.
암시적 any 상태인 변수에 어떤 할당도 하지 않고 사용하려고 하면 암시적 any 오류가 발생한다.
function range(start: number, limit: number) {
const out = [];
// 오류! 'out' 변수는 형식을 확인할 수 없는 경우
// 일부 위치에서 암시적으로 'any[]' 형식입니다.
if (start === limit) {
return out;
// 오류! 'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
}
for (let i = start; i < limit; i++) {
out.push(i);
}
return out;
}
any, unknown, never
할당 가능성의 관점에서 any 생각해보기
한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없기 때문에, any는 타입 시스템과 상충된다. 이는 바로 any의 강력함이면서 문제를 일으키는 원인이다. 타입 체커는 집합 기반이기 때문에 any를 사용하면 타입 체커가 무용지물이 된다.
unknown은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다. 어떠한 값이 있지만 그 타입을 모르는 경우에 unknown을 사용한다.
never는 unknown과 정반대다.
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown을 사용한다.
unknown 타입인 채로 값을 사용하면 오류가 발생한다. unknown을 사용하여 적절한 타입으로 변환하도록 강제할 수 있다.
unknown을 적절한 타입으로 바꾸는 방식은 다음 방법들이 있다.
unknown 대신 제너릭 매개변수가 사용되는 경우도 있다. 제너릭을 사용한 스타일은 달라보이지만 타입 단언문과 기능적으로는 동일하다. 제너릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다.
{}, object, unknown의 차이점을 이해하기
몽키 패치 - 런타임에 객체에 임의의 속성을 추가하는 것
자바스크립트에서는 window나 document에 값을 할당하여 전역 변수를 만들 수 있다. 이 경우 은연중에 프로그램 내에서 서로 멀리 떨어진 부분들 간에 의존성을 만들게 된다.
여기에 타입스크립트가 더해지면? 타입 체커는 Documet의 내장 속성은 알지만 임의로 추가된 속성을 알지 못한다. 그래서 any 단언문을 사용하게 된다. any를 사용함으로 타입 안전성을 잃고 언어 서비스를 사용할 수 없게 된다.
document.monkey = 'Tamarin';
// 오류! Property 'monkey' does not exist on type 'Document'.
(document as any).monkey = 'Tamarin'; // 정상
위 문제가 있을 때 최선의 해결책은 document 또는 DOM과 같은 객체로부터 데이터를 분리하는 것이다. 그러나 분리할 수 없는 경우에는 다음과 같은 차선책이 있다.
interface의 특수 기능 보강(augmentation) 사용하기 (아이템 13)
보강을 사용한 방법이 any보다 나은 점은 다음과 같다.
export {};
// 모듈의 관점(import/export를 사용하는 경우)에서
// 제대로 동작하려면 global 선언을 추가해야 한다.
declare global {
interface Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string;
}
}
document.monkey = 'Tamarin'; // 정상
더 구체적인 타입 단언문 사용하기
// Document 타입을 건드리지 않고 별도로 확장한다.
interface MonkeyDocument extends Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string;
}
(document as MonkeyDocument).monkey = 'Macaque';
// 새로운 타입을 도입했기 때문에 모듈 영역 문제가 해결된다.
noImplicitAny 설정과 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입은 여전히 프로그램 내에 존재할 수 있다.
명시적 any 타입
any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입은 코드 전반에 영향을 미친다. any[]
와 { [key: string] : any }
같은 타입은 인덱스를 생성하면 단순 any가 된다.
서드파티 타입 선언
@types
선언 파일로부터 any 타입이 전파된다.
npm의 type-coverage
패키지를 활용하여 any를 추적할 수 있다.
$ npx type-coverage
A / B C%
// B개 심벌 중 A개(C%)가 any가 아니거나 any의 별칭이 아닌 타입을 가진다.
$ npx type-coverage --detail
path/to/code.ts:1:10 getColumnInfo
path/to/module.ts:7:1 pt2
{[key: string]: any} 와 object 타입의 차이점
암시적 any 타입에 어떤 값을 할당 할 때 any 가 특정 타입으로 좁혀져서 진화한다.
하지만, 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생한다.
명시적으로 any 를 선언한 경우 any 진화가 일어나지 않는다.
암시적 any 타입은 함수를 호출해도 진화하지 않는다. 오로지 값 할당으로 진화한다.
any 를 진화시키는 방식 보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다!
document.monkey = 'Hey';
// Error
// 좋지 못한 해결방법1 any 단언문 사용하기
(document as any).monkey ='Hey';
interface 특수 기능인 '보강' 사용하기
interface Document{
monkey: string;
}
declare global {
interface Document{
monkey: string;
}
}
더 구체적인 타입 단언문 사용하기
interface MonkeyDocument extends Document{ monkey:string; }
(document as MonkeyDocument).monkey='hey';
# devDependencies 에 typescript 와 @types 추가하기
### dependenies
- 현재 프로젝트를 실행하는데 필수적인 라이브러리들
### devDependencies
- 현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들
### peerDependencies
- 런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리들
- 대표적인 예로 플로그인. 제이쿼리의 플러그인은 다양한 버전의 제이쿼리와 호환되므로 제이쿼리의 버전을 플러그인에서 직접 선택하지 않고 플러그인이 사용되는 실제 프로젝트에서 선택하도록 만들때 사용한다.
### 타입스크립트를 시스템 레벨로 설치하지 않는 이유
- 팀원들 모두가 항상 동일한 버전을 설치한다는 보장이 없다.
- 프로젝트를 셋업 할 때 별도의 단계가 추가된다.
# 타입 선언과 관련된 세 가지 버전 이해하기
세가지 버전
1. 라이브러리의 버전
2. 타입 선언(@types)의 버전
3. 타입스크립트의 버전
# TsDocs 사용하기
- 타입 정의에 주석처럼 사용하기!
- 너무 장황하게 쓰지 말기
- 익스포트 된 함수, 클래스, 타입에 주석을 달 때는 TSDocs 형태를 사용하기
- 주석에 타입 정보 포함하지 않기
# 콜백에서 this 에 대한 타입 제공하기
- 자바스크립트에서 let 이나 const 로 선언된 변수는 렉시컬 스코프이지만, this 는 동적 스코프이다.
- 즉, 동적 스코프의 값은 '정의된 방식'이 아니라 '호출된 방식'에 따라 달라진다!
```ts
class C {
vals = [1,2,3];
logSquares(){
for(const val of this.vals){
console.log(vals * vals);
}
}
}
const c = new C();
const method = c.logSquares;
method(); // error
그리서 아래와 같이 명시적으로 this 를 바인딩 해주어야 한다.
class C {
vals = [1,2,3];
logSquares(){
for(const val of this.vals){
console.log(vals * vals);
}
}
}
const c = new C();
const method = c.logSquares;
method.call(c); // 바인딩 ㅎㅎ
function addKeyListener{
el: HTMLElement,
fn: (this: HTMLElement, e: (KeyboardEvent)=>void )
}
function double(x: number | string): number | string;
function double(x:any){
return x+x;
}
function double<T extends number|string>(x:T):T; function double(x:any){ return x+x; }
const num = double(12); // 타입이 12 const str = double('x'); // 타입이 "x"
- 타입이 너무 과하게 구체적이다.
- 그러면 여러가지 타입 선언으로 분리해보쟈!!
```ts
function double(x:number):number;
function double(x:string):string;
function double(x:any){
return x+x;
}
const num = double(12); // 타입이 number
const str = double('x'); // 타입이 string
function double(x:number):number;
function double(x:string):string;
function double(x:any){
return x+x;
}
const num = double(12); // 타입이 12
const str = double('x'); // 타입이 "x"
function f(x:number|string){
return double(x); // Error 'string' | 'number' 형식의 인수는
// 'string' 형식의 매개변수에 할당될 수 없습니다!!
}
function double<T extends number | string>(x:T):T extends string ? string : number;
function double(x:any){
return x+x;
}
스폰지밥 마인드로 살아갑시다. 월요일 조아~🎵 https://www.youtube.com/watch?v=kLq3R2zCte8