JavaScript에서는 개체 또는 해당 프로토타입 체인에 해당 속성이 있는지 확인하는 in
연산자가 있다.
TypeScript에서는 잠재적 유형을 좁히는 방법으로 이 연산자를 사용할 수 있다.
type Fish = { swim : () => void };
type Bird = { fly : () =. void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly()
}
JavaScript에는 값이 다른 값의 인스턴스인지 여부를 확인하는 연산자가 있다.
보다 구체적으로 JavaScript에서 x instanceof Foo
는 x의 프로토타입 체인에 Foo.prototype이 포함되어 있는지 여부를 확인한다.
function logValue (x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
never
유형은 모든 유형에 할당할 수 있지만, 네버 유형에는 네버 자체를 제외하고는 다른 유형을 할당할 수 없다. 따라서 switch문에서 철저한 검사를 위해 사용할 수 있다.
type Shape = Circle | Square;
function getArea (shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
(a: string) => void
라는 문법은 "문자열 타입 a를 하나의 매개변수로 가지고 반환값이 없는 함수"를 의미한다.
타입 별칭을 사용해서 함수의 타입에 이름을 붙일 수도 있다.
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
Javascript에서 함수들은 호출이 가능할 뿐만 아니라, 프로퍼티도 가질 수 있다. 하지만 함수 타입 표현식 문법은 프로퍼티를 정의하는 것을 허락하지 않는다.
따라서 호출 가능하면서 프로퍼티를 가진 무언가를 설명하려고 하면 객체 타입에 호출 시그니처를 사용하여 표현할 수 있다.
이 문법은 함수 타입 표현식과 달리, 매개변수 타입과 반환값의 타입 사이에 =>
가 아닌 :
를 사용해야 한다.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
Javascript 함수는 new
연산자를 통해서도 호출될 수 있다. TypeScript는 이런 것들이 주로 새로운 객체를 생성하는데 사용되기 때문에 생성자로 간주한다.
호출 시그니처 앞에 new
키워드를 붙임으로서 구성 시그니처를 작성할 수 있다.
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
입력값이 출력값의 타입과 관련이 있거나, 두 입력값의 타입이 서로 관련있는 형태의 함수를 작성하는 것은 흔히 일어나는 일이다.
function firstElement(arr) {
return arr[0];
}
TypeScript에서 제네릭 문법은 두 값 사이의 상관관계를 표현하기 위해 사용된다. 함수 시그니처에서 타입 매개변수를 선언함으로써 제네릭 문법을 표현할 수 있다.
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
타입 매개변수 Type
을 이 함수에 선언하고, 필요한 두 곳에 사용함으로써 함수의 입력값과 출력값 사이에 연결고리를 만들 수 있다.
Type
을 특정하지 않고 TypeScript에 의해 자동적으로 추론되도록 할 수 있다.
여러 개의 타입 매개변수도 사용할 수 있다.
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
위에서 모든 타입에 대해 동작하는 제네릭 함수를 작성했다. 두 값을 연관시키기 원하지만 특정한 값들의 부분집합에 한해서만 동작하기를 원할 때가 있는데, 이러한 경우 타입 제한 조건을 사용하여 타입 매개변수가 받아들일 수 있는 타입들을 제한할 수 있다.
function longest<Type extends {length: number}>(a: Type, b: Type) {
if(a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray의 타입은 'number[]'이다.
const longerArray = longest([1,2], [1,2,3]);
// lognerString의 타입은 'alice' | 'bob'이다.
const longerString = longest('alice', 'bob');
// number에는 'length' 프로퍼티가 없으므로 에러가 난다.
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
const notOK = longest(10, 100);
Type
을 {length: number}
로 제한했기 때문에 a
와 b
매개변수에 대해 .length
프로퍼티에 접근할 수 있다. 타입 제한이 없다면 이런 값들이 length 프로퍼티를 가지지 않는 다른 타입일 수 있기 때문에 그 프로퍼티에 접근할 수 없었을 것이다.
결국 longest(10,100)
은 number 타입이 .length
프로퍼티를 가지고 있지 않기 때문에 호출이 거부된 것이다.
function minimumLength<Type extends {length: number}> (
obj: Type,
minimum: number
): Type {
if(obj.length >= minimum) {
return obj;
} else {
return {length: minimum};
// Type '{ length: number; }' is not assignable to type 'Type'.
// '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
Type
은 {length: number}
로 제한되어 있고, 함수는 Type
이나 제약조건을 만족하는 값을 반환하므로 문제가 없는 것처럼 보인다.
문제는 이 함수가 제약사항을 만족하는 어떤 객체가 아닌, 입력된 어떤 객체를 반환한다는 점이다.
이 코드가 유효하다면, 확실히 동작하지 않을 아래의 코드를 작성할 수 있을 것이다.
// 'arr' gets value {length: 6}
const arr = minimumLength([1,2,3], 6);
// 여기서 배열은 'slice' 메서드를 가지고 있지만, 반환된 객체는 그렇지 않기에 에러가 발생한다.
console.log(arr.slice(0));
TypeScript는 제네릭 호출에서 의도된 타입을 대체로 추론해 내지만, 항상 그렇지는 않다.
일반적으로 짝이 맞지 않는 배열과 함께 해당 함수를 부르는 것은 잘못된 것이다.
function combine<Type>(arr1: Type[], arr2: Type[]: Type[] {
return arr1.concat(arr2);
}
const arr = combine([1,2,3], ["hello"]);
// Type 'string' is not assignable to type 'number'.
만약 이게 의도한 것이라면, 수동으로 Type
을 명시해야 한다.
const arr = combine<string | number>([1,2,3], ["hello"]);
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// good
const a = firstElement1([1,2,3]);
// bad
const b = firstElement2([1,2,3]);
firstElement1
함수의 추론된 반환 타입은 Type
이지만, firstElement2
의 추론된 반환 타입은 TypeScript가 호출 중 타입을 해석하기 위해 기다리기 보단 호출 시점에 arr[0]
표현식을 타입 제한 조건을 이용해서 해석하기 때문에 any
가 된다.
가능하다면 타입 매개변수를 제약하기 보다 타입 매개변수 그 자체를 사용하기
// good
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
// bad
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
타입 매개변수 Func
는 타입 인수를 원하는 호출자가 아무 이유 없이 추가 타입 인수를 제공해야 하기 때문에 상당히 좋지 않다. Func
는 함수를 더 읽고 이해하기 어렵게 만들 뿐이지 아무것도 하지 않는다.
가능하다면 항상 타입 매개변수를 최소로 사용하기
가끔 함수가 제네릭이 필요 없을 수 있다는 사실을 간과한다.
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
이를 보다 쉽게 작성할 수 있다.
function greet(s: string) {
console.log("Hello, " + s);
}
타입 매개변수는 여러 값의 타입을 연관시키는 용도로 사용함을 기억해야 한다. 만약 타입 매개변수가 함수 시그니처에서 한 번만 사용되었다면, 어떤 것도 연관시키지 않고 있는 것이다.
만약 타입 매개변수가 한 곳에서만 나온다면, 정말로 필요한 것인지 다시 생각해보기
void
는 값을 반환하지 않는 함수의 반환값을 의미한다. 함수에 return문이 없거나, 명시적으로 값을 반환하지 않을 때 추론되는 타입이다.
Javascript에서는 아무것도 반환하지 않는 함수는 암묵적으로 undefined
값을 반환한다. 하지만 TypeScript에서 void
와 undefined
는 같은 것으로 간주되지 않는다.
특별한 타입 object
는 원시값이 아닌 모든 값을 지칭한다. 빈 객체 타입 { }
과는 다르고, 전역 타입 Object
와도 다르다.
object는 Object가 아니므로 항상 object를 사용해야 한다!
Javascript에서 함수 값은 객체이다. 프로퍼티가 있고 프로토타입 체인에 Object.prototype
이 있고, instanceof Object
이면서 Object.keys
를 호출할 수 있다. 이러한 이유료 TypeScript에서 함수 타입은 ojbect
로 간주된다.
unknown
타입은 모든 값을 나타낸다. any
와 유사하지만, unknown
타입에 어떤 것을 대입하는 것이 유효하지 않기 때문에 더 안전하다.
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// 'a' is of type 'unknown'.
}
any
형태의 값을 함수 본문에 사용하지 않고도 아무 값이나 받는 함수를 표현할 수 있기 때문에, 함수 타입을 설명하는 데에 유용하게 쓰인다.
반대로 unknown 타입의 값을 반환하는 함수를 표현할 수도 있다.
function safeParse(s: string): unknown {
return JSON.parse(s);
}
const obj = safeParse(someRandomString);
어떤 함수는 결코 값을 반환하지 않는다.
function fail(msg: string): never {
throw new Error(msg);
}
never
타입은 결코 관측될 수 없는 값을 의미한다. 반환 타입에서는 해당 함수가 예외를 발생시키거나, 프로그램 실행을 종료함을 의미한다.
never
은 TypeScript가 유니온에 아무것도 남아있지 않다고 판단했을 때 또한 나타난다.
function fn(x: string | number) {
if(typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // 'never' 타입!
}
}
전역 타입 Function
은 bind
, call
, apply
그리고 Javascript 함수 값에 있는 다른 프로퍼티를 설명하는 데에 사용된다. Function
타입의 값은 언제나 호출될 수 있다는 값을 가지며, 이러한 호출은 any
를 반환한다.
function doSomething(f: Function) {
return f(1, 2, 3);
}