TypeScript - Learning TypeScript Chap. 9 Type Modifier

이소라·2023년 4월 20일
0

TypeScript

목록 보기
23/28

Chapter 9. Type Modifiers

9.1 Top Types

  • top 타입은 시스템에서 가능한 모든 값을 나타내는 타입을 말합니다.
  • top 타입에 모든 타입을 할당할 수 있습니다.
    • top 타입에는 anyunknown이 있습니다.

9.1.1 any

  • any 타입은 모든 타입을 할당할 수 있습니다.
  • any는 일반적으로 console.log의 매개변수와 같이 모든 타입의 데이터를 받아들이는 위치에서 사용합니다.
let anyValue: any;
anyValue = 'abc';
anyValue = 123;
anyValue = true;

console.log(anyValue);
  • any 타입은 해당 값에 대한 할당 가능성과 멤버에 대해 타입 검사를 수행하지 않도록 명시적으로 지시한다는 문제점을 갖습니다.
    • 이러한 안정성 부족은 TypeScript의 타입 검사기를 빠르게 건너뛰려고 할 때 유용하지만, 타입 검사를 비활성화하면 TypeScript의 유용성이 줄어듭니다.
function greetComedian(name: any) {
  // 타입 오류 없음
  console.log(`Announcing ${name.toUpperCase()}!`);
}
// Runtime Error: name.toUpperCase is not a function
greetComedian({name: 'Authur'});

9.1.2 unknown

  • TypeScript에서 unknown 타입은 진정한 top 타입입니다.

  • unknown 타입은 모든 타입을 할당할 수 있습니다.

  • TypeScript는 unknown 타입의 값을 휠씬 더 제한적으로 취급합니다.

    1. TypeScript는 unknown 타입 값의 속성에 직접 접근할 수 없습니다.
    2. unknown 타입은 top 타입이 아닌 타입에는 할당할 수 없습니다.
  • 위의 두 가지 제한으로 인해 unknownany보다 훨씬 안전한 타입으로 사용됩니다.

  • unknown 타입 값의 속성에 접근하려고 시도하면, TypeScript는 타입 오류를 보고합니다.

function greetComedian(name: unknown) {
  // Error: 'name' is of type 'unknown'
  console.log(`Announcing ${name.toUpperCase()}!`);
}
  • TypeScript가 unknown 타입 값의 속성에 접근할 수 있는 유일한 방법은 instanceof나 typeof 또는 type assertion을 사용하는 것처럼 값의 타입이 제한된 경우입니다.
function greetComedianSafely(name: unknown) {
  if (typeof name === 'string') {
    // name: string
    console.log(`Announcing ${name.toUpperCase()}!`);
  } else {
    console.log('Get out');
  }
}

greetComedianSafely('White') // 'Announcing WHITE!'
greetComedianSafely(123); // 'Get out'
  • any 타입의 값은 모든 타입에 할당 가능하지만, unknown 타입의 값은 top 타입에만 할당 가능합니다.
let anyValue: any;
let unknownValue: unknown;
let str: string;
let num: number;

str = anyValue; // Ok
num = anyValue; // Ok
unknownValue = anyValue; // Ok

str = unknownValue; // Error: Type 'unknown' is not assignable to type 'string'
num = unknownValue; // Error: Type 'unknown' is not assignable to type 'number'
anyValue = unknownValue; // Ok



9.2 Type Predicates

  • instanceoftypeof와 같은 JavaScript 구문을 사용해 타입을 좁히는 방법은 제한된 검사로 이 방법을 직접 사용할 때는 괜찮지만 로직을 함수로 감싸면 타입을 좁힐 수 없게 됩니다.
function isNumberOrString(value: unknown) {
  return ['number', 'string'].includes(typeof value);
}

function logValueIfExists(value: number | string | null | undefined) {
  if (isNumberOrString(value)) {
    // value 타입 : number | string | null | undefined
    // Error: 'value' is possibly 'null' or 'undefined'.
    value.toString();
  } else {
    console.log('Value does not exist:', value);
  }
}
  • 타입 서술어(type predicate)는 인수가 특정 타입인지 여부를 나타내기 위해 boolean 값을 반환하는 함수를 위한 TypeScript의 특별한 구문입니다. 타입 서술어는 '사용자 정의 타입 가드(user-defined type guard)'라고도 부릅니다.

  • 타입 서술어는 일반적으로 매개변수로 전달된 인수가 매개변수의 타입보다 더 구체적인 타입인지 여부를 나타내는 데 사용됩니다.

  • 타입 서술어는 다음과 같이 서술됩니다.

function typePredicate(input: WideType): input is NarrowType;
function isNumberOrString(value: unknown): value is number | string {
  return ['number', 'string'].includes(typeof value);
}

function logValueIfExists(value: number | string | null | undefined) {
  if (isNumberOrString(value)) {
    // value 타입 : number | string
    value.toString();
  } else {
    // value 타입 : null | undefined
    console.log('Value does not exist:', value);
  }
}
  • 타입 서술어는 이미 한 interface의 인스턴스로 알려진 객체가 더 구체적인 interface의 인스턴스인지 여부를 검사하는데 자주 사용됩니다.
interface Comedian {
  funny: boolean;
}

interface StandupComedian extends Comedian {
  routine: string;
}

function isStandupComedian(value: Comedian): value is StandupComedian {
  return 'routine' in value;
}

function workWithComedian(value: Comedian) {
  if (isStandupComedian(value)) {
    // value : StandupComedian
    console.log(value.routine);
  }
  // value : Comedian
  console.log(value.routine);
  // Error: Property 'routine' does not exist on type 'Comedian'
}
  • 타입 서술어는 false 조건으로 타입을 좁히기 때문에 타입 서술어가 입력된 타입 이상을 검사하는 경우 예상치 못한 결과를 얻을 수 있습니다.
function isLongString(input: string | undefined) : input is string {
  return !!(input && input.length > 7);
}

function workingWithText(text: string | undefined) {
  if (isLongString(text)) {
    // text: string
    console.log('Long text:', text.length);
  } else {
    // text: undefined
    console.log('Short text:', text?.length);
    // Error: Property 'length' does not exist on type 'never'
  }
}



9.3 Type Operators

9.3.1 keyof

  • JavaScript 객체는 일반적으로 string 타입인 동적값을 사용하여 검색된 멤버를 갖습니다.
  • TypeScript의 타입 시스템에서 string 같은 포괄적인 원시 타입을 사용하면 컨테이너 값에 대해 유효하지 않은 키가 허용됩니다.
    • 엄격한 구성 설정을 사용할 때, TypeScript는 string 같은 포괄적인 원시 타입을 키로 사용하면 오류를 보고합니다.
interface Ratings {
  audience: number;
  critics: number;
}

function getRating(ratings: Ratings, key: string): number {
  // Error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Ratings'.
  //	No index signature with a parameter of type 'string' was found on type 'Ratings'.
  return ratings[key];
}

const ratings: Ratings = { audience: 66, critics: 84 };
// Ok
getRating(ratings, 'audience');
// 허용되지만 사용하면 안 됨
getRating(ratings, 'not vaild');
  • string 키를 사용하고자 하면, 인덱스 시그니처를 선언해야 합니다.
interface Ratings {
  audience: number;
  critics: number;
  [i: string]: number;
}

function getRating(ratings: Ratings, key: string): number {
  // Ok
  return ratings[key];
}

const ratings: Ratings = { audience: 66, critics: 84 };
// Ok
getRating(ratings, 'audience');
// Ok
getRating(ratings, 'not vaild');
  • 리터럴 union 타입을 사용하여 컨테이너 값에 존재하는 키만 적절하게 제한할 수 있습니다.
interface Ratings {
  audience: number;
  critics: number;
}

function getRating(ratings: Ratings, key: 'audience' | 'critics'): number {
  // Ok
  return ratings[key];
}

const ratings: Ratings = { audience: 66, critics: 84 };
// Ok
getRating(ratings, 'audience');
// Error: Argument of type 'not vaild' is not assignable to parameter type 'audience' | 'critics'
getRating(ratings, 'not vaild');
  • interface에 수십 개 이상의 멤버가 있다면, 각 멤버의 키를 union 타입으로 모두 입력하고 최신 상태를 유지해야 하는 것이 번거로울 수 있습니다.

  • TypeScript에서는 기존에 존재하는 타입을 사용하고, 해당 타입에 허용되는 모든 키의 union 타입을 반환하는 keyof 연산자를 제공합니다.

    • 타입을 사용하는 모든 곳의 타입 이름 앞에 keyof 연산자를 배치할 수 있습니다.
function getCountKeyof(ratings: Ratings, key: keyof Ratings) {
  // Ok
  return ratings[key];
}

const ratings: Ratings = { audience: 66, critics: 84 };

getRating(ratings, 'audience');
// Error: Argument of type 'not vaild' is not assignable to parameter type 'keyof Rating'
getRating(ratings, 'not vaild');

9.3.2 typeof

  • TypeScript에서 제공하는 타입 연산자 typeof는 제공되는 값의 타입을 반환합니다.
    • typeof는 값의 타입이 복잡한 경우에 사용하면 매우 유용합니다.
const original = {
  medium: 'movie',
  title: 'Mean Girls'
};

let adaptation: typeof original;

if (Math.random() > 0.5) {
  adaptation = {... original, medium: 'play'};
} else {
  // Error: Type 'number' is not assignable to type 'string'
  adaptation = {... original, medium: 2};
}

Note

  • TypeScript의 typeof 연산자와 JavaScript의 typeof 연산자는 다릅니다.
    • JavaScript의 typeof 연산자 : 타입에 대한 문자열을 반환하는 런타임 연산자
    • TypeScript의 typeof 연산자 : 값의 타입을 반환하는 타입 연산자

keyof typeof

  • TypeScript는 keyoftypeof 키워드를 함께 연결해 값의 타입에 허용된 키를 검색할 수 있습니다.
const ratings = {
  imdb: 8.4,
  metacritic: 82,
};

function logRating(key: keyof typeof ratings) {
  console.log(ratings[key]);
}

// Ok
logRating('imdb');
// Error: Argument of type '"invalid"' is not assignable to parameter of type '"imdb" | "metacritic"'.
logRating('invalid');
  • keyoftypeof를 결합하여 명시적 interface 타입이 없는 객체에 허용된 키를 나타내는 타입에 대한 코드를 작성하고 업데이트하는 수고를 줄일 수 있습니다.



9.4 Type Assertions

  • TypeScript는 값의 타입에 대한 타입 시스템의 이해를 재정의하기 위한 구문으로 type assertion(또는 type cast)을 제공합니다.

  • type assertion은 다른 타입을 의미하는 값의 타입 다음에 as 키워드를 배치하여 작성합니다.

    • 타입 시스템은 assertion을 따라 값을 해당 타입으로 처리합니다.
  • 예를 들어 JSON.parse는 의도적으로 top 타입인 any를 반환합니다. JSON.parse에 주어진 특정 문자열 값이 특정한 값 타입을 반환해야 한다는 것을 type assertion을 사용하여 타입 시스템에 알릴 수 있습니다.

const rawData = ['grace', 'frankie'];

// 타입 : any
JSON.parse(rawData);
// 타입 : string[]
JSON.parse(rawData) as string[];
// 타입 : [string, string]
JSON.parse(rawData) as [string, string];
// 타입 : ['grace', 'frankie']
JSON.parse(rawData) as ['grace', 'frankie'];
  • type assertion은 TypeScript 타입 시스템에만 존재하며 JavaScript로 컴파일 될 때 다른 타입 시스템 구문과 함께 제거됩니다.
const rawData = ['grace', 'frankie'];

// 타입 : any
JSON.parse(rawData);
// 타입 : string[]
JSON.parse(rawData);
// 타입 : [string, string]
JSON.parse(rawData);
// 타입 : ['grace', 'frankie']
JSON.parse(rawData);
  • TypeScript 모범 사례는 가능한 한 type assertion을 사용하지 않는 것입니다.

9.4.1 Asserting Caught Error Types

  • 오류를 처리할 때 type assertion이 매우 유용할 수 있습니다.
  • 코드 영역이 Error 클래스의 인스턴스를 발생시킬 거라 틀림없이 확신한다면 type assertion을 사용해 포착한 오류를 Error 클래스의 인스턴스 타입으로 처리할 수 있습니다.
try {
  // 오류를 발생시키는 코드
} catch (error) {
  console.warn('Oh no!', (error as Error).message);
}
  • 발생된 오류가 예상된 오류 타입인지를 확인하기 위해 instanceof 검사와 같은 type narrowing을 사용하는 것이 더 안전합니다.
try {
  // 오류를 발생시키는 코드
} catch (error) {
  console.warn('Oh no!', error instanceof Error ? error.message : error);
}

9.4.2 Non-Null Assertions

  • 이론적으로만 null 또는 undefined를 포함할 수 있는 변수에서 null과 undefined를 제거할 때 type assertion을 주로 사용합니다.
    • null과 undefined를 제외한 값의 전체 타입을 작성하는 대신 !를 사용하여 non-null assertion을 할 수 있습니다.
// maybeDate: Date | undefined
let maybeDate = Math.random() > 0.5 ? undefined : new Date();
// maybeDate: Date
maybeDate as Date;
// maybeDate: Date
maybeDate!;
  • non-null assertion은 값을 반환하거나 또는 존재하지 않는 경우 undefined를 반환하는 Map.get과 같은 API에서 특히 유용합니다.
const seasonCounts = new Map([
  ['I love Lucy', 'Me too'],
  ['The Golden Girls', 'Yep'],
]);


const maybeValue = seasonCounts.get('I love Lucy');
// Error: 'maybeValue' is possibly 'undefined'.
console.log(maybeValue.toUpperCase());

const maybeValue = seasonCounts.get('I love Lucy')!;
// Ok
console.log(maybeValue.toUpperCase());

9.4.3 Type Assertion Caveats

  • type assertion은 any 타입과 마찬가지로 타입 시스템에 필요한 하나의 도피수단입니다.

    • 꼭 필요한 경우가 아니라면 가능한 한 사용하지 말아야 합니다.
  • type assertion은 종종 잘못되기도 합니다. 작성 당시 이미 잘못되었거나 코드베이스가 변경됨에 따라 나중에 달라지기도 합니다.

const seasonCounts = new Map([
  ['I love Lucy', 'Me too'],
  ['The Golden Girls', 'Yep'],
]);

const maybeValue = seasonCounts.get('I love Lucy')!;
// Ok
console.log(maybeValue.toUpperCase());
// Runtime TypeError: Cannot read property 'toUpperCase' of undefined.
  • type assertion은 자주 사용하면 안 되고, 사용하는 것이 안전하다고 확실히 확신할 때만 사용해야 합니다.

assertion vs declaration

  • 변수 타입을 선언하기 위해 type annotation을 사용하는 것과 초깃값으로 변수 타입을 변경하기 위해 type assertion을 사용하는 것 사이에는 차이가 있습니다.
    • 변수의 type annotation과 초깃값이 모두 있을 때, TypeScript의 타입 검사기는 변수의 type annotation에 대한 변수의 초깃값에 대해 할당 가능성을 검사합니다.
    • 그러나 type assertion은 타입 검사 중 일부를 건너뛰도록 명시적으로 지시합니다.
interface Entertainer {
  acts: string[];
  name: string;
}

const declared: Entertainer = {
  // Error: Property 'acts' is missing in type { name: string } but required in type 'Entertainer'.
  name: 'Mons Mabley',
};

// 허용되지만 런타임 시 오류 발생
const asserted = {
  name: 'Mons Mabley',
} as Entertainer;

// Runtime Error: Cannot read properties of undefined
console.log(declared.acts.join(', '));
console.log(asserted.acts.join(', '));
  • 따라서 type annotation을 사용하거나 TypeScript가 초깃값에서 변수의 타입을 유추하도록 하는 것이 매우 바람직합니다.

Assignability of type assertion

  • TypeScript는 타입 중 하나가 다른 타입에 할당 가능한 경우에만 두 타입 간의 type assertion을 허용합니다.
    • 완전히 서로 관련 없는 두 타입 사이에 type assertion이 있는 경우에는 TypeScript가 타입 오류를 감지하고 알려줍니다.
    • 예를 들어, 원시 타입은 서로 관련이 없으므로 하나의 원시 타입에서 다른 원시 타입으로 전환하는 것은 허용되지 않습니다.
// Error: Conversion of type 'string' to type 'number' may be a mistake 
// because neither type sufficiently overlaps with the other. 
// If this was intentional, convert the expression to 'unknown' first.
let myValue = 'Stella' as number;
  • 하나의 타입에서 값을 완전히 관련 없는 타입으로 전환해야 하는 경우, double type assertion을 사용합니다.
    • 먼저 값을 anyunknown같은 top 타입으로 전환한 다음, 그 결과를 관련 없는 타입으로 전환합니다.
let myValue = '1337' as unknown as number; // 허용되지만 이렇게 사용하면 안 됨
  • as unknown as... double type assertion은 위험하고 거의 항상 코드의 타입이 잘못되었다는 징후를 나타냅니다.
  • 타입 시스템의 도피 수단으로 double type assertion을 사용하면, 주변 코드를 변경해서 이전에 작동하던 코드에 문제가 발생할 경우, 타입 시스템이 여러분을 구해주지 못할 수 있음을 의미합니다.



9.5 Const Assertions

  • const assertion은 배열, 원시 타입, 값, 별칭 등 모든 값을 상수로 취급해야 함을 나타내는 데 사용합니다.

  • as const는 수신하는 모든 타입에 다음 3 가지 규칙을 적용합니다.

    1. 배열은 가변 배열이 아니라 읽기 전용 튜플로 취급됩니다.
    2. 리터럴은 일반적인 원시 타입과 동등하지 않고 리터럴로 취급됩니다.
    3. 객체의 속성은 읽기 전용으로 간주됩니다.
  • 배열 리터럴 뒤에 as const사 배치되면 배열이 튜플로 처리되어야 함을 나타냅니다.

// 타입 : (number | string)[]
[0, ''];
// 타입 : readonly [0, '']
[0, ''] as const;

9.5.1 Literals to Primitives

  • 타입 시스템이 리터럴 값을 원시 타입으로 확장하기보다 특정 리터럴로 이해하는 것이 유용할 때 리터럴에 const assertion을 사용합니다.

  • 예를 들어 특정 리터럴을 생성한다고 알려진 함수에서 const assertion을 유용하게 사용할 수 있습니다.

// 타입 : () => string
const getName = () => 'Maria Bamford';
// 타입 : () => 'Maria Bamford'
const getNameCost = () => 'Maria Bamford' as const;
  • 값의 특정 필드가 더 구체적인 리터럴 값을 갖도록 할 때도 const assertion을 유용하게 사용할 수 있습니다.
    • 많은 인기 있는 라이브러리는 값의 판별 필드가 특정 리터럴이 되도록 요구합니다.
    • 따라서 해당 코드의 타입이 값을 더 구체적으로 추론할 수 있습니다.
interface Joke {
  quote: string;
  style: 'story' | 'one-liner';
}

function tellJoke(joke: Joke) {
  if (joke.style === 'one-liner') {
    console.log(joke.quote);
  } else {
    console.log(joke.quote.split('\n'));
  }
}

const narrowJoke = {
  quote: 'If you stay alive for no other reason do it for spite.',
  style: 'one-liner' as const,
}

// Ok
tellJoke(narrowJoke);

const wideObject = {
  quote: 'Time flies when you are anxious!',
  style: 'one-liner',
};

// Error: Argument of type '{ quote: string; style: string; }' is not assignable to parameter of type 'Joke'.
//  Types of property 'style' are incompatible.
//    Type 'string' is not assignable to type '"story" | "one-liner"'.
tellJoke(wideObject);

9.5.2 Read-Only Objects

  • 객체 리터럴은 let 변수의 초깃값이 확장되는 것과 동일한 방식으로 속성 타입을 확장합니다.
    • apply 같은 문자열 값은 string과 같은 원시 타입이 되고, 배열은 튜플이 아닌 array가 됩니다.
  • 객체 리터럴에 const assertion을 적용하면, 해당 값 리터럴이 변경되지 않고 모든 멤버에 동일한 const assertion 로직이 재귀적으로 적용됩니다.
    • 모든 멤버 속성이 readonly가 되고, 리터럴은 일반적인 원시 타입 대신 고유한 리터럴 타입으로 간주되며, 배열은 읽기 전용 튜플이 됩니다.
function describePreference(preference: 'maybe' | 'no' | 'yes') {
  switch (preference) {
    case 'maybe':
      return 'I suppose...';
    case 'no':
      return 'No thanks.';
    case 'yes':
      return 'Yes please!';
  }
}

// 타입 : { movie: string, standup: string }
const preferencesMutable = {
  movie: 'maybe',
  standup: 'yes',
};

// Error: 
function describePreference(preference: 'maybe' | 'no' | 'yes') {
  switch (preference) {
    case 'maybe':
      return 'I suppose...';
    case 'no':
      return 'No thanks.';
    case 'yes':
      return 'Yes please!';
  }
}

// 타입 : { movie: string, standup: string }
const preferencesMutable = {
  movie: 'maybe',
  standup: 'yes',
};
// Error: Argument of type '{ movie: string; standup: string; }' is not assignable to parameter of type '"maybe" | "no" | "yes"'.
describePreference(preferencesMutable);

// 타입 : { readonly movie: 'maybe', readonly standup: 'yes' }
const preferencesReadonly = {
  movie: 'maybe',
  standup: 'yes',
} as const;
// Ok
describePreference(preferencesReadonly);
// Error: Cannot assign to 'movie' because it is a read-only property.
preferencesReadonly.movie = 'no';

0개의 댓글