본문 바로가기
Web/JavaScript

[TypeScript] Indexed Access Types, Mapped Types, Conditional Types

by llHoYall 2021. 7. 9.

Indexed Access Type

We can use Indexed Access Type to look up a specific property on another type.

type Person = { age: number; name: string; alive: boolean };

let age: Person["age"] = 18;
console.log(typeof age);
// number

The indexing type is itself a type, so we can use union, keyof, or other types entirely.

type Person = { age: number; name: string; alive: boolean };

let person1: Person["age" | "name"] = 18;
console.log(typeof person1);
// number

person1 = "HoYa";
console.log(typeof person1);
// string

let person2: Person[keyof Person] = true;
console.log(typeof person2);
// boolean

If you try to index a property that doesn't exist, you will see an error.

This is the power of TypeScript and the reason why we use TypeScript.

 

Another example of indexing with an arbitrary type is using number to get the type of an array's elements. We can combine this with typeof to conveniently capture the element type of an array literal.

const testArray = [
  { name: "HoYa", age: 18 },
  { name: "Thomas", age: 23 },
  { name: "John", age: 31 },
];

type Person = typeof testArray[number];
let person: Person = { name: "Test", age: 7 };

type Name = Person["name"];
let testName: Name = "index";

type Age = typeof testArray[number]["age"];
let age: Age = 18;

Mapped Type

You probably don't want to repeat similarities when you create a type based on a different type.

Mapped Type builds on the syntax for index signatures, which are used to declare the types of properties which has not been declared ahead of time.

type MyTypes = {
  [key: string]: number | boolean;
};

let testVar: MyTypes = {
  test1: true,
  test2: 7,
};

Mapped Type is a generic type that uses a union created via a keyof to iterate through the keys of one type to create another.

type Person = {
  name: string;
  age: number;
};

type Optional<T> = {
  [P in keyof T]?: T[P];
};

let person: Optional<Person> = { name: "HoYa" };

In this example, we take all the properties from the type and change the types of their values to optional.

 

You can remove or add modifiers by prefixing with - or +. If you don't add a prefix, then + is assumed.

type ReadOnlyPerson = {
  name: string;
  readonly age: number;
};

type Modifiable<T> = {
  -readonly [P in keyof T]: T[P];
};

let person1: ReadOnlyPerson = { name: "HoYa", age: 18 };
// person1.age = 7; => Error: Cannot assign to 'age' because it is a read-only property

let person2: Modifiable<ReadOnlyPerson> = { name: "Kim", age: 32 };
person2.age = 3;

 

You cal also remap keys in mapped types with an as clause in Mapped Type.

type ToGetter<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type Person = {
  name: string;
  age: number;
  address: string;
};

let person: ToGetter<Person> = {
  getName: () => "HoYa",
  getAge: () => 18,
  getAddress: () => "Seoul",
};
type RemoveAddress<T> = {
  [P in keyof T as Exclude<P, "address">]: T[P];
};

type Person = {
  name: string;
  age: number;
  address: string;
};

let person: RemoveAddress<Person> = {
  name: "HoYa",
  age: 18,
};

But if you want to use this feature, you have to use TypeScript version 4.1 or later.

 

Mapped Type works well with other features.

type ExtendedWithBoolean<T> = {
  [P in keyof T]: T[P] extends { ext: true } ? true : false;
};

type Person = {
  name: { type: string; ext: true };
  age: { type: number; ext: false };
};

let person: ExtendedWithBoolean<Person> = {
  name: true,
  age: false,
};

Conditional Type

Conditional Type takes a form that looks a little like conditional expressions in JavaScript.

Here is an example of Conditional Type.

interface Shape {
  draw(): void;
}

interface Rectangle extends Shape {
  setWidth(): void;
}

type TestType = Rectangle extends Shape ? string : number;

let testVar: TestType = "test";

The type of testVar is string since the Rectangle interface is inherited from the Shape interface.

 

The power of Conditional Type comes from using them with Generics.

interface IName {
  name: string;
}

interface IAge {
  age: number;
}

type Person<T extends number | string> = T extends string ? IName : IAge;

function makePerson<T extends number | string>(value: T): Person<T> {
  throw new Error("Unimplemented");
}

/*
Same as followings

function makePerson(value: string): IName;
function makePerson(value: number): IAge;
function makePerson(value: string | number): IName | IAge;
function makePerson(value: string | number): IName | IAge {
  throw new Error("Unimplemented");
}
*/

let person1 = makePerson("HoYa"); // IName
let person2 = makePerson(7); // IAge
let person3 = makePerson(Math.random() ? "Park" : 3); // IName | IAge

 

In this example, we could flatten array types to their element types, but leaves them alone otherwise.

type Flatten<T> = T extends any[] ? T[number] : T;

let strVar: Flatten<string[]> = "apple";
let numVar: Flatten<number> = 7;

 

Conditional type provides us with a way to infer from types we compare against in the true branch using the infer keyword.

We can write some useful helper type aliases using the infer keyword.

ttype GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;

type MyNumber = GetReturnType<() => number>;
// number

type MyString = GetReturnType<(x : string) => string>;
// string

type MyBool = GetReturnType<(x: boolean) => boolean[]>;
// boolean[]

'Web > JavaScript' 카테고리의 다른 글

[Svelte] Store  (0) 2021.11.13
[Svelte] Lifecycle  (0) 2021.11.12
[TypeScript] Mixins  (0) 2021.07.09
[TypeScript] Generic  (0) 2021.07.05
[TypeScript] Singleton Pattern  (0) 2021.07.05

댓글