본문 바로가기

개발/TS

[TS] TS Doc Handbook 1회독 - 6

Type Manipulation

Creating Types from Types

  • Generics
function echo<Type>(arg: Type): Type {
  return arg;
}

let output = echo<string>('Test'); // string 출력
let output2 = echo('Test'); // string 출력, 타입 추론

function echoArr<Type>(arg: Type[]): Type[] {
// 같은 의미
// function echoArr<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length);
  return arg;
}

let output3 = echoArr([1,2,3]); // 3, [1,2,3] 출력

△제네릭 함수

let myEcho: { <Type>(arg: Type): Type } = echo;

// or

interface GenericEchoFn {
  <Type>(arg: Type): Type;
}

let myEcho: GenericEchoFn = echo;

// or

interface GenericEchoFn<Type> {
  (arg: Type): Type;
}

let myEcho: GeneircEchoFn<string> = echo;

// do not
// because TS parser doesn't support a naked generic arrow function
let myEcho: <Type>(arg: Type) => Type;

△ 제네릭 타입

class GenericClass<Type> {
  val: type;
  func: (x: Type, y: Type) => Type;
}

let myGenericClass = new GenericClass<string>();
myGenericClass.val = 'test';
myGenericClass.func = function (x,y) {
  return x + y;
};

△ 제네릭 클래스(인스턴스 측면에서만 사용)

 

제네릭 열거형(enums)과 네임스페이스(namespace)는 만들 수 없습니다.

 

제네릭 제약조건

// length 속성을 가진 타입으로 제한하고 싶은 경우
interface ExtendLength {
  length: number;
}

function Test<Type extends ExtendLength>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

 

 

function createInstance<Type>(c: {new (): Type}): Type {
  return new c();
}

 

  • Keyof Type Operator

keyof 연산자는 객체 타입에서 객체의 키 값을 리터럴 유니언을 생성

type Test = {x: number; y: string};
type T = keyof Test; // 'x' | 'y'

type Test = { [n: number]: unknown };
type T = keyof Test; // number

type Test = { [s: string]: boolean };
type T = keyof Test; // string | number
// string | number인 이유는 객체 키는 항상 문자열이기 때문에 obj[0]과 obj['0']은 동일

 

  • Typeof Type Operator

typeof 연산자는 해당 변수나 속성의 타입을 추론할 수 있다.

console.log(typeof 'str'); // string

 

// 예를 들어 ReturnType<T>을 사용하면 함수 타입을 받아 반환 타입을 제공합니다.
type Func = (x: unknown) => string;
type R = ReturnType<Func>; // string;

// 하지만 함수 이름을 넣으면 오류가 납니다.
function func() {
  return { x:10, y:'oh'};
}
type R = ReturnType<func>;
// error: 'func' refers to a value, but is being used as a type here. Did you mean 'typeof func'?

// 이유는 값과 타입은 같지 않다!!
// == 함수 값인 func과 func의 타입인 typeof func의 차이가 있다.
type R = ReturnType<typeof func>; // {x:number; y:string;}

 

typeof 키워드의 제한 사항으로 식별자 또는 속성(property)에서만 사용할 수 있다.

 

  • Indexed Access Types

특정 속성을 찾기 위한 인덱싱 접근

type Test = {val: number; str: string; bool: boolean;};
type Val = Test['val']; // number
type I1 = Test['val' | 'bool']; // number | boolean
type I2 = Test[keyof Test]; // number | string | boolean

// 배열의 인덱스를 number로 접근해서 가져올 수 있다.
const Arr = [{str:'str', num: 10}];
type T = typeof Arr[number]; // {str:string; num:number;}

// 인덱싱을 할 때는 값을 사용할 수 없고, 타입만 사용가능하다.
const key = 'val';
type Val = Test[key]; // 오류 발생

 

  • Conditional Types

삼항 연산자 조건문과 같은 형태로 타입에도 사용할 수 있다.

// 왼쪽 타입(Some)이 오른쪽 타입(Thing)에 할당할 수 있다면 참
type Example = Some extends Thing ? number : string;

// 제네릭과 함께 사용하면 좋다
type One {
  val: number;
}
type More {
  val: string;
}
type OneOrMore<T extends number | string> = T extends number ? One : More;

function createSome<T extends number | string>(oneOrMore: T): OneOrMore<T> {
  throw 'unimplemented';
}

 

infer 키워드를 사용하면 '참' 값 분기에서 비교하는 타입을 추론할 수 있는 것 같다. 그런데 어떻게 사용하는지는 아직 이해불가

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

 

여러 호출 시그니처를 가진 타입을 추론할 때 마지막 시그니처로 추론

유니언 타입을 제네릭에 넘기게 되면 각 타입에 조건이 적용. 방지하기 위해선 extends 키워드의 양 옆으로 []로 감싸기

type ToArr<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

// 분산 방지
type ToArray<Type> = [Type] extends [any] ? Type[] : never; // (string | number)[]

 

  • Mapped Types
type Example = {
  [key: type] : type;
}

type GenericExample<Type> = {
  [property in keyof Type]: type;
  // -,+를 통해 readonly 수정자 추가 가능. 기본값은 +
  readonly [property in keyof Type]; Type[Property];
  -readonly [property in keyof Type]; Type[Property];
  
  // -,+를 통해 ?(optional) 수정자 추가 가능. 기본값은 +
  [property in keyof Type]-?: type;
  [property in keyof Type]?: type;
  
  // TS 버전 4.1 이상에서는 as를 사용해 별칭 가능
  [property in keyof Type as something]: type;
  
  // 조건부 타입을 통해 never를 생성하고 키 필터링
  [property in keyof Type as Exclude<property, ''>]: Type[property];
}

 

  • Template Literal Types

기존 JS에서 사용되는 템플릿 리터럴 문자열과 같은 문법을 사용하지만 type에서 사용한다는 점이 다르다.

type Some = 'some';
type Thing = `${Some} thing`; // 'some thing' 타입

// union이 사용된 타입은 전부 적용된다.
type Some = 'some' | 'other';
type Thing = 'thing' | 'Thing';

type UnionType = `${Some | Thing} new!`; // "some new!" | "other new!" | "thing new!" | "Thing new!"
type UnionTyp2 = `${Some}_${Thing}`; // "some_thing" | "some_Thing" | "other_thing" | "other_Thing"

 

예를 들어 기존 함수에 on 함수를 추가하고 싶은데 해당 함수의 첫 인자가 기존 함수가 가진 객체 타입 속성값에 Changed만 추가된 경우라면 템플릿 리터럴 타입을 활용하면 쉽게 확인할 수 있다.

// 해당 타입에서 `${string & keyof Type}Changed`를 이용하면
// 쉽게 기존 속성 이름에 Changed만 추가할 수 있다.
type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};

declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.on("firstNameChanged", () => {});
 
person.on("firstName", () => {}); // error 속성 이름이 잘못되었다.

 

그다음은 기존 방식으로는 newValue의 타입이 any가 아닌 string으로 전환하고 싶다면 할 수 있는 방법이 나오는데 한 번에 이해할 수 없을 것 같아 넘깁니다.

 

타입에서 조작할 수 있는 방법들

  • Uppercase<StringType>: 대문자로 변경
  • Lowercase<StringType>: 소문자로 변경
  • Capitalize<Stringtype>: 시작 글자 대문자로 변경
  • Uncapitalize<Stringtype>: 시작 글자 소문자로 변경

 

참고

https://www.typescriptlang.org/docs/handbook/2/types-from-types.html

'개발 > TS' 카테고리의 다른 글

[TS] TS Doc Handbook 1회독 - 8  (0) 2025.06.20
[TS] TS Doc Handbook 1회독 - 7  (0) 2025.06.17
[TS] TS Doc Handbook 1회독 - 5  (1) 2025.06.13
[TS] TS Doc Handbook 1회독 - 4  (0) 2025.03.04
[TS] TS Doc Handbook 1회독 - 3  (0) 2025.02.28