[typescript] Narrowing

dev stefanCho·2021년 7월 18일
1

typescript

목록 보기
3/16

Narrowing은 condition branches(조건문)마다, type을 명확하게 하는 것이다. 이때 interface, type, non-null assertions, union types, never, as operator, is operator 등이 사용될 수 있다. 각 조건에서 타입을 명확하게 하여, 빈틈이 없도록 하는 것이다.

Narrowing

type guard

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가지 중 하나로 예상한다.

  • string
  • number
  • bigint
  • boolean
  • symbol
  • undefined
  • object
  • function
typeof null 		// object
typeof function(){} // function
typeof 1n			// bigint

Truthiness Narrowing

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로 필터링 하는 것이다.

Equality Narrowing

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 으로 narrowing

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)이 다 존재할 수도 있다. 즉, 모든 조건문을 만족시키게 할 수 있다.

instanceof narrowing

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');
}

Control flow analysis

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만이 존재할 수 있다.

Using type predicates

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 Winterseason is Fall은 서로 다르지 않다. is는 단지 boolean type일 뿐이다. return type에 boolean 대신 is operator 를 쓰는 이유는 개발할 때 potential 문제를 방지할 수 있는 이점이 있기 때문이다. (개발상의 이점)

Discriminated unions

구분되는 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(!)가 없더라도 아무 문제가 없다.

The never type

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.kindnever이고, 이것은 nevernever를 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;
  }
}

defaultshape.kindnever이 아니기 때문에, assign할 수 없다.

Ref

Typescript handbook: Narrowing

profile
Front-end Developer

0개의 댓글