Type Manipulation[Typescript]

SnowCat·2023년 2월 16일
0

Typescript - Handbook

목록 보기
7/9
post-thumbnail

Generics

  • 다양한 타입에서 작동하는 컴포넌트를 만들어야 할 때 제네릭을 사용가능
function identity(arg: number): number {
  return arg;
}

// 제네릭이 없다면 any로 처리해야하며, 반환시 타입 정보를 잃어버리게 됨
function identity(arg: any): any {
  return arg; // 입력 타입에 상관없이 any
}

// 제네릭 사용
function identity<Type>(arg: Type): Type {
  return arg;  // 입력 타입에 맞는 타입 반환
}

//타입 명시
let output = identity<string>("myString"); //string
//타입 추론
let output = identity("123"); //number

Working with Generic Type Variables

  • 제네릭을 사용하게 되면 타입스크립트에서는 내부에서 제네릭들이 any나 모든 타입이 될 수 있는 것처럼 변수를 취급하게 됨
function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // Property 'length' does not exist on type 'Type'.
  return arg;
}

// 타입 범위를 배열로 좁혀줘야 함
function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length); // ok
  return arg;
}

Generic Types

  • 일반 함수와 유사하게 제네릭 함수 선언 가능
function identity<Type>(arg: Type): Type {
  return arg;
}

// 제네릭의 이름은 임의로 정해도 상관 없음
let myIdentity: <Input>(arg: Input) => Input = identity;

// 리터럴 타입의 호출 시그니처로도 작성 가능
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;

// 인터페이스로도 사용 가능
interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;

Generic Classes

  • 클래스에도 제네릭을 사용 가능
class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};

Generic Constraints

  • interface를 상속받아 타입 범위를 제약시킬 수 있음
interface Lengthwise {
  length: number;
}
 
// type에는 length 속성이 있는 것들만 들어올 수 있음
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]);
loggingIdentity(3); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
loggingIdentity({ length: 10, value: 3 });
  • 다른 타입 매개변수를 가지고 타입 범위를 제약시킬수도 있음
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
getProperty(x, "m"); 

Using Class Types in Generics

  • 제니릭을 사용하는 팩토리를 생성할 때 생성자 함수로는 클래스 타입을 참조해야 함
function create<Type>(c: { new (): Type }): Type {
  return new c();
}

class BeeKeeper {
  hasMask: boolean;
}
 
class ZooKeeper {
  nametag: string;
}
 
class Animal {
  numLegs: number;
}
 
class Bee extends Animal {
  keeper: BeeKeeper;
}
 
class Lion extends Animal {
  keeper: ZooKeeper;
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

Keyof Type Operator

  • keyof 연산자는 객체 타입에서 객체의 키 값을 가지는 타입 유니언을 반환함
type Point = { x: number; y: number };
type P = keyof Point; // "x" | "y" 타입

// 인덱스 시그니쳐를 가지면, 해당 타입 반환
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;

// string이 키값으로 들어오면 타입은 string | number로 변환됨
// 자바스크립트에서 객체 키는 항상 문자열이 되기 때문에 obj[0](=obj["0"])처럼 숫자가 들어올 수 있기 때문
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;

Typeof Type Operator

  • 자바스크립트에는 typeof 연산자가 내장되어 있음
console.log(typeof "Hello world"); //string

let s = "hello";
let n: typeof s; // n: string

// 변수 혹은 프로퍼티에서만 사용 가능
// ',' expected.
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
  • 타입스크립트에서는 함수의 반환값의 타입을 제공받는 ReturnType 타입이 있음
type Predicate = (x: unknown) => boolean;
//ReturnType<T>, 타입이 아닌 값을 반환받음에 주목
type K = ReturnType<Predicate>; //k = boolean

function f() {
  return { x: 10, y: 3 };
}

// 'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?
type P = ReturnType<f>;

// f의 값을 추론하기 위해서는 typeof 사용 필요
type P = ReturnType<typeof f>;
/*type P = {
    x: number;
    y: number;
}
*/
  • typeof를 통해 타입 추론시 모든 케이스에 허용되는 넓은 범위로 타입추론을 하게 됨. 즉, 인자 타입의 목록에 기반해서 오버로드를 처리할수는 없음
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>; // string | number

Conditional Types

  • 삼항 연산자 조건문을 활용해 조건에 따라 타입을 줄 수도 있음
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string; //number
 
type Example2 = RegExp extends Animal ? number : string; //string
  • 제네릭과 결합하여 코드의 양을 줄일 수 있음
// 2개의 타입을 결합하려고만 해도 코드가 매우 길어짐
interface IdLabel {
  id: number /* some fields */;
}
interface NameLabel {
  name: string /* other fields */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

// 제네릭과 조건부 타입 사용
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
  • 조건부 타입을 사용해 타입 범위를 좁힐수도 있음
// 메시지 여부에 따라 메시지 속성의 타입이나 never 반환
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>; //string
type DogMessageContents = MessageOf<Dog>; //never

// 배열의 타입을 평탄화시키는 예시
type Flatten<T> = T extends any[] ? T[number] : T;
 
type Str = Flatten<string[]>; //string
type Num = Flatten<number>; //number
  • 조건부 타입 내부에서 infer 키워드를 사용해서 임의의 타입을 선언적으로 사용할 수 있음
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
 
type Num = GetReturnType<() => number>; //number
 
type Str = GetReturnType<(x: string) => string>; //string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; //boolean[]
  • 제네릭 타입 위에서 조건부 타입은 유니언 타입을 만나면 분산적으로 동작함
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; //string[], number

//(string | number)[]로 동작시키려면 extend 키워드 양쪽을 대괄호로 감싸야 함
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
type StrArrOrNumArr = ToArrayNonDist<string | number>; //(string | number)[]

Mapped Types

  • 중복을 피하기 위해서 다른 타입을 바탕으로 새로운 타입을 생성 가능
type OnlyBoolsAndHorses = {
  [key: string]: boolean | Horse;
};

// OnlyBoolsAndHorses 타입 바탕으로 새로운 타입 생성
const conforms: OnlyBoolsAndHorses = {
  del: true,
  rodney: false,
};


type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
};
 
//OptionFlags에 의해 타입이 맵핑되 FeatureFlags 속성을 가지고 OptionsFlags의 값을 가지게 됨 
type FeatureOptions = OptionsFlags<FeatureFlags>;

// ex
type FeatureOptions = {
    darkMode: boolean;
    newUserProfile: boolean;
}
  • readonly, 물음표 수정자를 사용할 수 있고, - 또는 + 접두사를 붙여 수정자를 추가하거나 없엘 수 있음 (없을시 +로 간주)
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
// LockedAccount에서 readonly가 사라진 타입
type UnlockedAccount = CreateMutable<LockedAccount>;

type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
 
type MaybeUser = {
  id: string;
  name?: string;
  age?: number;
};

// name, age는 더이상 optional이 아님
type User = Concrete<MaybeUser>;
  • 매핑된 타이에 as를 사용해 타입의 키를 다시 맵핑할 수도 있음
type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}


//모든 타입의 조합을 임의로 매핑 가능
type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}
 
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
 
type Config = EventConfig<SquareEvent | CircleEvent>


// as 타입을 사용해 kind 속성을 제거하는 예시
// Exclude<a, b> -> a 타입에서 b에 해당되는 타입을 제거한 타입 반환
type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle"; // Exclude에 의해 제외됨
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;

Template Literal Types

  • 백틱을 사용하는 템플릿 리터럴을 타입을 표현할때도 사용할 수 있음
type Greeting = `hello ${World}`; //type: hello World

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
// type: "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

type Lang = "en" | "ja" | "pt";
// 12개의 타입 조합이 가능
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;

String Unions in Types

  • 문자열이 규칙적으로 입력되야 할 때 유용하게 템플릿 리터럴 타입 사용 가능
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});

person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

// firstNameChanged, lastNameChanged, ageChanged 식으로 값이 들어오면 템플릿 리터럴을 사용해볼 수 있다.
//여전히 any가 사용되고 있음에 주목
type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};

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

person.on("firstNameChanged", () => {}); //ok
person.on("firstName", () => {}); //error
person.on("frstNameChanged", () => {}); //error

// any를 없에기 위해 generic 사용해 완성
type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 
const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.on("firstNameChanged", newName => {
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

Intrinsic String Manipulation Types

  • 타입스크립트에는 문자열을 조작할 수 있는 몇가지 내장 타입이 있음
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting> //type: "HELLO, WORLD"

type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting> //type: "hello, world"

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>; //type: "Hello, world"

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>; //type: "hELLO, WORLD"

출처:
https://www.typescriptlang.org/ko/docs/handbook/2/generics.html

profile
냐아아아아아아아아앙

0개의 댓글