이 글은 유튜브 "Fun Times with Advanced TypeScript"의 영삼을 참고하였음을 알려드립니다.
또, 완전히 이해하고 적은 영상이 아니여서 뭐라하지 말아주세요..
함수에 어떠한 타입이 올 지 모를 때 <> 를 사용하여 타입을 지정합니다.
타입의 이름은 자유롭게 작성이 가능하며 관례상 대문자로 작성합니다.
type Duck = Quackable & Swimmable;
또는
interface User {
name: string;
}
interface Action {
do(): void;
}
function createUserAction(u: User, a: Action) : User & Action { // intersection types
return { ...u, ...a };
}
const u = createUserAction({ name: "Lee" }, { do() {} });
type Flayble = Eagle | Butterfly;
let strOrNum: string | number;
strOrNum = 1 // OK
strOrNum = "Lee" // OK
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>.pet).swim();
} else {
(<Bird>.pet).fly();
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// pet is Fish : 타입 가드
// pet as Fish : 타입 단언
in
operatorin
연산자는 타입을 좁히는 표현으로 작용합니다.
n in x
표현에서 n
은 문자열 리터럴 혹은 문자열 리터럴 타입
x
는 유니온 타입
function move(pet: Fish | Bird) {
if ("swim" in pet) {
// "swim" 을 필수 혹은 선택적으로 가지는 타입으로 좁힘
return pet.swim(); // 있으면 swim
}
return pet.fly(); // 없으면 fly
}
타입스크립트는 typeof
도 타입 가드로 인식하기 때문에 함수로 추상화 할 필요없이 인라인으로 검사할 수 있습니다.
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
// isNumber(x: any): x is number => typeof x === "number"; // OK
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
// isString(x: any): x is string => typeof x === "string"; // OK
return padding + value;
}
throw new Error("Expected string or number, got `${padding}`.");
}
typeof
타입 가드의 형식인 typeof v === "typename"
와 typeof v !== "typename"
이 있습니다.
이 때 typename
은 "number", "string", "boolean", "symbol"
이어야 합니다. TS에서 위에 없는 문자열과 비교하는 것을 막지는 않지만, 타입 가드의 표현식으로 인지하지 않습니다.
const canfly = (animal: Animal): animal is Flyable
=> typeof (animal as any).fly === "function";
// animal의 타입이 any고
// fly 메서드의 타입이 function 일 때 true를 반환합니다.
if (canFly(animal)) {
animal.fly();
}
person.children[0]!.name;
null
또는 undefined
를 제거해준다고 하는데 아직까지 정확한 활용법을 모르겠다.
구체적인 예시로
let name: (string | null);
name.charAt(0) // Error, 'name'이 null 일 수 있기 때문
name = name || "Lee" // 여기선 "Lee" 로 바뀜
// 그래서 아래와 같이 느낌표(type assertion, 타입 단언)을 넣어주면 됨
name!.charAt(0) // Good
// 확정적 타입 단언
// 확정적 타입 단언은 프로퍼티의 인스턴스를 !를 이용해 사용할 수 있게해주는 것? 정확하게는 모르겠다.
let bool = true;
const arr = [1, 2, 3];
const tuple = [true, 1];
bool = 1; // Error!
위의 예제에서 각 변수의 타입은 적절하게 추론되므로 다시 사용할 때 Type Safe 하게 사용할 수 있다.
bool의 타입은 boolean으로 추론되므로 number 타입인 1을 할당하려고 하면 에러가 발생한다.
arr 변수는 배열에 숫자만 담겨져 있는 걸로 봐서 number[]로 추론된다. 이 때 배열의 길이는 고정이 아닌 것으로 추론한다.
아래의 tuple도 마찬가지로 배열로 추론된다. 튜플로 사용하고 싶다면 별 수 없이 타입을 선언해주어야 한다. 다만 배열의 요소가 각각 boolean과 number 이기에 최종적으로 추론되는 타입은 boolean과 number의 유니온 타입의 배열, (boolean | number)
이다.
여기서 배열에서 사용된 요소들의 타입을 각각 추론하여 유니온 타입으로 만들어 내는 방식을 Best common type 이라고 부른다.
말 그대로 가장 일반적인 타입.
여러 가지 타입이 사용될 때 그 타입을 포괄할 수 있는 가장 일반적인 타입을 추론하는 것이다.
위의 예제에서는 true와 1을 포괄할 수 있는 타입인 (boolean | number)
가 추론된 것.
// 스트링 자체를 타입으로 사용할 수 있다.
type Move = "ROCK" | "PAPER" | "SICSSOR";
const throws = (): never => {
throw new Error("This never returns"); // 리턴값이 절대 없음
};
const loops = (): never => {
while (true) {
console.log("This never returns either"); // 예외가 절대 없음
}
};
let a: string;
let x: any;
a = x; // Compiles
x = a; // Compiles
let y: unkown;
y = a; // Compiles
a = y; // Does net compile
// unknown 타입은 any 타입과 마찬가지로 모든 타입이 할당 될 수 있다.
// 그러나, unknown 타입으로 선언된 변수는 any를 제외한 다른 타입으로 선언된 변수에 할당될 수 없다.
// 또 프로퍼티에 접근 불가능
y.foo.bar // Error: Object is of type 'unknown'.
y.method() // Error
y() // Error
new y() // Error
// 타입 가드와 함께라면 접근 가능하다.
let variable: unknown
declare function isFunction(x: unknown): x is Function
if (isFunction(variable)) {
variable(); // OK
}
인덱스로 접근하는 타입
type Duck = {
colors: string;
feathers: number;
}
type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type colorType = Duck['colors']; // = string
type DuckValues = Duck[DuckProps] // string | number
조건을 이용하여 타입을 지정하는 것
type StringOrNumber<T> = T extends boolean ? string : number;
type T1 = StringOrNumber<true> // string
type T2 = StringOrNumber<false> // string
type T3 = StringOrNumber<Object> // number
// In application
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string> // "string"
type ElementType<T> = T extends (infer U)[] ? U : never;
type T = ElementType<[]> // = never
type T1 = ElementType<string[]> // = string
// 만약 T가 '어떤 것'의 배열이면 타입은 '어떤 것'
// 그렇지 않으면 타입은 'never'
interface RawPerson {
identifier: number;
first_name: string;
last_name: string;
}
interface Person {
id: strin;
fullName: string;
}
const transformPerson = (raw: RawPerson): Person => {
return {
id: `${raw.identifier}`,
fullName: `${raw.first_name} ${raw.last_name}`,
}
}
서로 다른 값과 형식을 가진 몇몇 사례 중 하나 일 수 있는 값을 말합니다.
약간 reducer같다고 저는 생각했습니다.
type Eagle = {
kind: "eagle";
fly: () => "fly";
};
type Duck = {
kind: "duck";
quack: () => "quack";
};
type Bird = {
kind: "bird";
};
type Animal = Eagle | Duck | Bird;
const assertNever = (animal: Animal): never => {
throw new Error(`Unknown animal ${animal.kind}`);
}
const doSomething = (animal: Animal): string => {
switch(animal.kind) {
case "eagle":
return animal.fly();
case "duck":
return animal.quack();
default:
return assertNever(animal);
}
};
뭔지 잘 모르겠네요.. ㅜㅜ
const MOVES = {
ROCK: { beats: "SCISSOR" },
PAPER: { beats: "ROCK" },
SCISSOR: { beats: "PAPER" },
};
type Move = keyof typeof MOVES;
const move: Move = "ROCK";
// 유저의 인풋을 믿지 못할 때 사용하는 타입?
const validateInt = (s: unknown): number => {
let n;
switch (typeof s) {
case "number":
// handle
case "string":
// handle
default:
throw new Error("Not a number.");
}
}
읽기 전용으로 만들 때
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>;
// = { readonly color: string; readonly feathers: number }
부분 조건으로 만들 때
type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>;
// = { color?: string; feathers?: number }
필수 조건으로 만들 때
type PartialDuck = {
color?: string;
feathers?: number;
}
type Required<T> = { [P in keyof T]-?: T[P] }; // - (required)
type Duck = Required<PartialDuck>;
type Nullable<T> = { [P in keyof T]: T[P] | null }
;
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type ColorDuck = Pick<Duck, "color">;
// = { color: string }
아직 이해하지 못하는거.
재네릭을 겹치는 것.
Promise<Pick<Reaction , T>> 과 같이 제네릭이 겹칠 때
그 프로세스를 모르겠음.
약간 흐름이 어떻게 가는지...
async fucntion fetchPersonById<T extends keyof Person> (
id: string,
...fields: T[]
): Promise<Pick<Reaction, T>> {
return awiat knex("Person")
.where({ id })
.select(fields)
.first();
}
const reaction = awiat fetchPersonById(id, "name", "age");
// = { name: string, age: number }
이해 못함
type Record<K extends string, T> = { [P in K]: T };
type Day = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
type EverydayParty = Record<Day>;
type Filter<T, U> = T extends U ? T : never;
type T1 = Filter<"a" | "b", "a"> // = "a"
// Native code 같음
type T2 = Extract<"a" | "b", "a"> // = "a"
type Diff<T, U> = T extends U ? never : T;
type T1 = Diff<"a" | "b", "a"> // = "b"
// Native code 같음
type T2 = Exclude<"a" | "b", "a"> // = "b"
type NonNullable<T> = Diff<T, null | undefined>;
// Diff -> T 로 받은 타입이 null 또는 undefined 의 확장된 것인가?
// 확장된 것이면 -> return never
// 확장된 것이 아니면 -> return T
type T = NonNullable<string | null | undefined>;
// 따라서 string은 null 또는 undefined 의 확장된 것이 아니기 때문에 string
// = string
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type partialDuck = Omit<Duck, "feathers">;
// = { color: string }
export interface WithUserProps {
user: User;
}
export const withUser = <P extends WithUserProps>
(Component: React.ComponentType<P>) => (props: Omit<P, WithUserProps>) => {
<Component {...props} user={getUser()} />
}
const UsernameComponent = ({ user, message }: { user: User, message: string }) => (
<div>Hi {user.username}! {message}</div>
}
const Username = withUser(UsernameComponent); // ({ message }) => JSX.Element
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type Duck = {
color: string,
fly: () => void
}
type T1 = FunctionProperties<Duck>;
// = { fly: () => void }
type T2 = FunctionPropertyNames<Duck>; // = "fly"
type NonFunctionPropertyNames<T> = {
[k in keyof T]: T[K] extends Function ? never : K
}[keyof T]
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNams<T>>;
type Duck = {
color: string,
fly: () => void,
}
type T1 = NonFunctionProperties<Duck>; // = { color: string }
type T2 = NonFunctionPropertyNames<Duck>; // = { color }
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
const x = (a: string): number => 1;
type R = ReturnType<typeof x>; // = number
import { ClientOpts, createClient } from 'redis';
import { promisify } from 'util';
export const promisifyRedis = (opts: ClientOpts) => {
const redis = createClient(opts);
const promisifiedRedis = {
setx: promisify(redis.setex),
get: promisify(redis.get),
};
const wrappedRedis: typeof promisifiedRedis = { } as any;
for (const key of Object.keys(promisifiedRedis) as (keyof typeof promisifiedRedis)[]) {
wrappedRedis[key] = promisifiedRedis[key].bind(redis);
}
return wrappedRedis;
};
export type Redis = ReturnType<typeof promisifiedRedis>;
/* =
type Redis = {
setex: (arg1: string, arg2: number, arg3: string) => Promise(string);
get: (arg1: string) => Promise<...>;
}
*/
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
class Duck { }
const make2 = <T extends new (...args: any[]) => any>
(constructor: T): [InstanceType<T>, InstanceType<T>] =>
[new constructor(), new constructor()]
const ducks = make2(Duck); // = [Duck, Duck]
고급 타입 을 보고 느낌점
너무 어렵다. 참고로 나는 타스를 한 지 별로 안된 신생아 수준이기 때문에 여러 키워드에서 애를 먹고 있다. ex) extends, keyof, is, as, forin 등..
열심히 하자
글에 오류가 있으면 댓글 달아주세요.
TypeScript intersection & union Types
Fun Times with Advanced TypeScript
Unknown Type