any
의 장점은 살리면서 단점을 줄이는 방법들에 대해서 알아보자.
1. 함수와 관련된 any
function processBar(b: Bar){ // ... }
function f(){
const x = expressionReturningFoo();
processBar(x); // 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
// 문맥상으로 x라는 변수가 동시에 Foo 타입과 Bar타입에 할당 가능하다면,
// 오류 제거 방법이 두가지 있다.
// 1. f1은 함수의 마지막까지 x의 타입이 any -> 다른 코드에 영향 o
function f1(){
const x: any = expressionReturningFoo(); // 이렇게 하지 맙시다!
processBar(x);
}
// 2. processBar 호출 이후에 x가 그대로 Foo 타입 -> 다른 코드에 영향 x
function f2(){
const x = expressionReturningFoo();
processBar(x as any); // 이게 낫습니다.
}
// 3. @ts-ignore 사용 -> any를 사용하지 않고 오류를 제거할 수 있지만 근본적인 해결책 x
function f1(){
const x = expressionReturningFoo();
// @ts-ignore
processBar(x);
return x;
}
2. 객체와 관련된 any
// 어떤 큰 객체 안의 한 개 속성이 타입 오류를 가지는 상황
const config: Config = {
a: 1,
b: 2,
c: {
key: value
// 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
}
};
// 1. config 객체 전체를 as any로 선언해서 오류 제거
// -> a,b 속성 타입 체크 불가능
const config: Config = {
a: 1,
b: 2,
c: {
key: value
}
} as any; // 이렇게 하지 맙시다!
// 2. 최소한의 범위에만 any 사용 -> a,b 속성 타입 체크 가능
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any
}
};
🎯 요약
최소한의 범위에서만 any를 사용하자
// 함수 타입에도 단순히 any를 사용해서는 안 된다.
// 최소한으로나마 구체화할 수 있는 세 가지 방법
type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: ang[]) => any; // 모든 개수의 매개변수 "Function" 타입과 동일합니다.
// (any[] 로 선언하면 배열 형태라는 것을 알 수 있어서 구체적임)
// any[] 타입을 사용하는 가장 일반적인 경우
const numArgsBad = (...args:any) => args.length; // any를 반환
const numArgsGood = (...args: any[]) => args.length; // number를 반환
🎯 요약
최소한의 범위에서만 any를 사용하자
declare function shallowEqual(a: any, b: any): boolean
// 원본 함수 T타입과 동일한 매개변수로 호출되고, 반환 값 역시 같기 때문에 타입 단언문 추가
function cacheLast<T extends Function>(fn: T): T {
let lastArgs: any[] | null = null
let lastResult: any
return function (...args: any[]) {
if (!lastArgs || !shallowEqual(lastArgs, args)) {
lastResult = fn(...args)
lastArgs = args
}
return lastResult
} as unknown as T
}
// 함수 내부에는 any가 많이 사용됐지만,
// cahceLast를 호출하는 쪽에서는 any가 사용되었는지 알지 못한다.
declare function shallowEqual(a: any, b: any): boolean
function shallowObjectEqual<T extends object>(a: T, b: T): boolean {
for (const [k, aVal] of Object.entries(a)) {
// k in b 체크로 b가 k 속성이 있다는 것을 확인했지만, b[k]에서 오류 발생
// 따라서 b as any 로 단언
if (!(k in b) || aVal !== (b as any)[k]) {
return false
}
}
return Object.keys(a).length === Object.keys(b).length
}
🎯 요약
any를 사용해야 한다면, 함수 안으로 숨기자
타입의 진화(evolve)가 일어나는 경우
- 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화함
- 조건문에서는 분기에 따라 타입이 변할 수 있음
- 변수의 초깃값이 null인 경우
noImplicitAny
가 설정된 상태에서 변수의 타입이 암시적인 any인 경우
(단, 명시적으로 any를 선언하면 타입이 그대로 유지됨)- 암시적 any 타입에 어떤 값을 할당할 때만 발생하고, 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생
타입의 진화는 타입 좁히기와 다르다.
// 1. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화함
const result = []; // 타입이 any[]
result.push('a');
result; // 타입이 string[]
result.push(1);
result; // 타입이 (string|number)[]
// 3. 변수의 초깃값이 null인 경우도 any의 진화가 일어남(보통 try/catch문)
let val = null; // 타입이 any
try {
somethingDangerous();
val = 12;
val; // 타입이 number
}catch(e){
console.warn('alas!');
}
val // 타입이 number | null
🎯 요약
any와 any[]는 일반적인 타입과 다르게 진화가 가능하므로 동작이 발생하는 코드를 인지하고 이해하자
unknown
에는 함수의 반환값과 관련된 형태, 변수 선언과 관련된 형태, 단언문과 관련된 형태가 있다.1. 함수의 반환값과 관련된 unknown
// 이상적 반환값 타입 할당
function parseYAML(yaml: string): any {
// ...
}
interface Book {
name: string
author: string
}
const book: Book = parseYAML(`
name: Jane Eyre
author: Charlotte Brontë
`)
// 1. any 사용
function parseYAML(yaml: string): any {
// ...
}
interface Book {
name: string
author: string
}
// 함수의 반환값에 타입 선언을 강제할 수 없기 때문에,
// 호출한 곳에서 타입 선언 생략하여 book 변수는 암시적 any
// 따라서, 사용되는 곳 마다 에러가 발생한다.
const book = parseYAML(`
name: Jane Eyre
author: Charlotte Brontë
`)
alert(book.title) // 오류 없음, 런타임에 "undefined" 경고
book('read') // 오류 없음, 런타임에 "TypeError: book 은 함수가 아닙니다" 예외 발생
// 2. unknown 사용
// any 대신 쓰자 -> any는 타입 시스템과 상충되는 면을 가지고 있음
// (한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없다)
interface Book {
name: string
author: string
}
function safeParseYAML(yaml: string): unknown {
return parseYAML(yaml)
}
// unknown 타입인 채로 값을 사용하면 오류가 발생하므로 적절한 타입으로 강제 변환
const book = safeParseYAML(`
name: Villette
author: Charlotte Brontë
`) as Book
alert(book.title)
// ~~~~~ 'Book' 형식에 'title' 속성이 없습니다.
book('read')
// ~~~~~~~~~ 이 식은 호출할 수 없습니다.
2. 변수 선언과 관련된 unknown
어떠한 값이 있지만 그 타입을 모르는 경우에 unknown 사용
unknown 에서 원하는 타입으로 변환하기
타입 단언문
instanceof로 체크
interface Geometry {}
function processValue(val: unknown) {
if (val instanceof Date) {
val // Type is Date
}
}
interface Geometry {}
function isBook(val: unknown): val is Book {
return typeof val === 'object' && val !== null && 'name' in val && 'author' in val
}
function processValue(val: unknown) {
if (isBook(val)) {
val // Type is Book
}
}
3. 단언문과 관련된 unknown
interface Foo {
foo: string
}
interface Bar {
bar: string
}
declare const foo: Foo
// barAny와 barUnk는 기능적으로 동일하지만,
// 나중에 두 개의 단언문을 분리할 때, unknwon 형태인 barUnk가 더 안전하다.
let barAny = foo as any as Bar
let barUnk = foo as unknown as Bar // 분리되는 즉시 오류를 발생
{}
타입은 null과 undefined를 제외한 모든 값을 포함한다.object
타입은 모든 비기본형 타입(객체, 배열,함수 등)으로 이루어진다.🎯 요약
any보다는 unknown이 안전하다.
window
또는DOM 노드
에 데이터를 추가한다고 가정
-> 데이터는 전역 변수가 되고, 함수를 호출할 때마다 부작용을 고려해야 함
1. 문제점
타입 체커는 Document와 HTMLElement
의 내장 속성에 대해서는 알지만, 임의로 추가한 속성(monkey)은 모른다
document.monkey = 'Tamarin'
// ~~~~~~ 'Document' 유형에 'monkey' 속성이 없습니다.
2. 해결책
2-1. any 단언문 사용
단점 - any를 사용해서 타입 안정성이 불안하고, 언어 서비스를 이용하지 못한다.
(document as any).monkey = 'Tamarin' // 정상
(document as any).monky = 'Tamarin' // 정상으로 나오지만 오타 있음
(document as any).monkey = /Tamarin/ // 정상으로 나오지만 잘못된 타입
2-2. Document또는 DOM
으로부터 데이터를 분리(최선책)
😑 내장 타입과 데이터 분리가 어려운 상황이라면...?
2-3. interface의 기능인 보강을 사용(차선책)
주의 - 모듈 영역(scope), 보강은 전역적으로 적용되기 때문에 코드의 다른 부분이나 라이브러리부터 분리할 수 없다.
애플리케이션이 실행되는 동안 속성을 할당하면, 실행 시점에서 보강을 적용할 방법이 없다.
interface Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string
}
document.monkey = 'Tamarin' // 정상
// 모듈의 관점에서 제대로 동작하게 하려면, global 선언을 추가
export {}
declare global {
interface Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string
}
}
document.monkey = 'Tamarin' // 정상
2-4. 더 구체적인 타입 단언문 사용(차선책)
// 1. MonkeyDocument는 Document를 확장하기 떄문에 타입 단언문은 정상이며 할담문의 타입도 안전
// 2. Document 영역을 건드리지 않고 확장하여 새로운 타입을 도임해서 모듈 영역 문제도 해결
interface MonkeyDocument extends Document {
/** 몽키 패치의 속(genus) 또는 종(species) */
monkey: string
}
(document as MonkeyDocument).monkey = 'Macaque'
🎯 요약
몽키 패치보다는 안전한 방법으로 타입을 사용하자
any 타입이 프로그램내에 존재 할 수 있는 경우
1. 명시적 any 타입 - any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any
2. 서드파티 타입 선언 -@types
선언 파일로부터 any 타입이 전파되기 때문에 조심해야 함.
noImplicitAny
를 설정했어도, any 타입은 여전히 코드 전반에 영향을 미침
any의 개수를 추적하여 타입 안정성을 높이자
npx type-coverage
9985 / 10117 98.69%
-> 프로젝트의 10117개 심벌 중 9985개(98.69%)가 any가 아니거나 any의 별칭이 아닌 타입을 가지고 있다.
npx type-coverage --detail
-> any 타입이 있는 곳을 모두 출력
🎯 요약
any를 추적하여 타입 안정성을 높이자