Narrowing은 condition branches(조건문)마다, type을 명확하게 하는 것이다. 이때 interface, type, non-null assertions, union types, never, as operator, is operator 등이 사용될 수 있다. 각 조건에서 타입을 명확하게 하여, 빈틈이 없도록 하는 것이다.
if문으로 typeof를 보고 특별한 형태의 타입이라고 판단하는 것을 부르는 말
In TypeScript, checking against the value returned by typeof is a type guard.
// number 타입을 보고 특정한 타입으로 보는 것
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
typeof을 이용한 type guards
typeof의 값은 다음 8가지 중 하나로 예상한다.
typeof null // object
typeof function(){} // function
typeof 1n // bigint
typeof null은 object이다.
This is one of those unfortunate accidents of history
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) { // Object는 null일 수도 있다.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
type을 체크한 object가 null일 수 있기 때문에, Truthiness narrowing을 한다. 쉽게말해 true, false로 필터링 하는 것이다.
TypeScript also uses switch statements and equality checks like ===, !==, ==, and != to narrow types.
여기서 null == undefined 는 true 이므로, looser equality(==, !=)는 제대로 check를 못할 수 있다.
function printAll(strs: string | string[] | null) {
if (strs === null) return; // 바로 위의 코드에서 이것만 추가됨
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
위에서 if (!strs) return; 으로 쓰지 않는 이유는, 빈 문자열('')가 걸리기 때문이다. 우리는 정확히 null만 골라내야 한다!
in
operator는 object의 property가 있는 지 체크할 수 있다.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?:() => void, fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ('swim' in animal) {
return animal.swim?.();
}
return animal.fly?.();
}
const man = {
swim: () => {
console.log('The man is swimming');
},
fly: () => {
console.log('He is Superman!');
}
}
const woman = {
fly: () => {
console.log('She has a private Airplane!');
}
}
move(man); // The man is swimming
move(woman); // She has a private Airplane!
optional property으로 양쪽(swim, fly)이 다 존재할 수도 있다. 즉, 모든 조건문을 만족시키게 할 수 있다.
As you might have guessed, instanceof is also a type guard, and TypeScript narrows in branches guarded by instanceofs.
typeof와 마찬가지로 instanceof는 어떤 value의 instance인지 판단하여 narrowing 할 수 있다.
Syntax
object instanceof constructor
class Man {
name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
}
const stefan = new Man('stefan cho');
if (stefan instanceof Man) {
console.log('Stefan is a man');
} else {
console.log('Stefan is a woman');
}
This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.
control flow에 따라 type을 제한시킬 수 있다.
function getX(x: string | number | boolean) {
if (x === 'good') {
x = 'buy'
} else {
x = 0;
}
return x; // (parameter) x: string | number
}
parameter x
의 타입은 3가지로 받을 수 있지만, return은 string과 number만이 존재할 수 있다.
return type에 is
operator로 type을 예측할 수 있다.
type Spring = { flower: string };
type Summer = { beach: string };
type Fall = { mountain: string };
type Winter = { resort: string }
function winterResort(season: Spring | Summer | Fall | Winter) {
return (season as Winter).resort;
}
function isWinter(season: Spring | Summer | Fall | Winter): season is Winter {
return typeof (season as Winter).resort === 'string';
}
Runtime에서는 season is Winter
와 season is Fall
은 서로 다르지 않다. is
는 단지 boolean type일 뿐이다. return type에 boolean
대신 is
operator 를 쓰는 이유는 개발할 때 potential 문제를 방지할 수 있는 이점이 있기 때문이다. (개발상의 이점)
구분되는 unions를 의미함, 공통되는 property를 갖고 있는 상태에서 unions를 제대로 구분해주기 위해서 사용된다. (제대로 이해하기 위해서는 아래 예시를 참고)
interface Shape {
kind: 'circle' | 'square';
radious?: number;
sideLength?: number;
}
const getArea = (shape: Shape) => {
if (shape.kind === 'circle') {
return Math.PI * shape.radious ** 2; // Object is possibly 'undefined'.(2532)
}
if (shape.kind === 'square') {
return shape.sideLength ** 2; // Object is possibly 'undefined'.(2532)
}
return null;
}
const getArea2 = (shape: Shape) => {
if (shape.kind === 'circle') {
return Math.PI * shape.radious! ** 2;
}
if (shape.kind === 'square') {
return shape.sideLength! ** 2;
}
return null;
}
getArea
는 ts error가 발생한다. property가 optional(?
)이기 때문에, 없을 수도 있다는 것이다. getArea2
는 non-null assertions(!
)을 추가했기 때문에 ts error가 발생하지 않는다.
getArea2
처럼 쓰게되면, 당장은 문제가 없지만 코드를 다른곳으로 이동하거나 리팩토링할 경우 문제가 생길 수 있다.
interface Circle {
kind: 'circle';
radious: number;
}
interface Square {
kind: 'square';
sideLength: number;
}
type Shape = Circle | Square;
const getArea3 = (shape: Shape) => {
if (shape.kind === 'circle') {
return Math.PI * shape.radious ** 2;
}
if (shape.kind === 'square') {
return shape.sideLength ** 2;
}
return null;
}
interface
를 두가지로 정확히 분리하고, union type
을 만들었다. getArea3
에는 non-null asssertions(!
)가 없더라도 아무 문제가 없다.
never
은 어떤타입에든 assign할 수 있다. (never에는 never만 assign 가능하다. 즉, never = never
형태만 가능), switch case문에서 사용할 수 있다.
type Shape = {
kind: 'circle' | 'square'
}
const getShape = (shape: Shape) => {
switch (shape.kind) {
case 'circle':
return 'shape is circle';
case 'square':
return 'shape is sqaure';
default:
const exhaustiveCheck: never = shape.kind;
return exhaustiveCheck;
}
}
shape.kind
는 never
이고, 이것은 never
에 never
를 assign하는 형태이므로, 문제가 없다.
참고로 const exhaustiveCheck: never = shape
는 에러가 발생하는데, shape
자체는 never
가 아니기 때문이다. 반면에 shape.kind
는 위 case문에서 literal type을 모두 소진했으므로 never
이다.
type Shape = {
kind: 'circle' | 'square' | 'rect'
}
const getShape = (shape: Shape) => {
switch (shape.kind) {
case 'circle':
return 'shape is circle';
case 'square':
return 'shape is sqaure';
default:
const exhaustiveCheck: never = shape.kind; // Type 'string' is not assignable to type 'never'.(2322)
return exhaustiveCheck;
}
}
default
에 shape.kind
가 never
이 아니기 때문에, assign할 수 없다.