narrow; 좁은, 좁히다
함수 내부로 들어온 인자에 대한 Type검사를 통해 조건을 좁혀서 특정 행동을 하는 것을 통칭하여 TypeScript에서는 Narrowing이라고 한다.
위 코드에서 if에서 넘어온 매개변수의 타입을 검사함으로써 if안에서는 arg1이 number
타입으로 사용될 것이고, else안에서는 string
타입으로 사용될 것이다.
이처럼 if나 type검사를 통해 조건을 좁히고 특정 코드를 수행할 때 더 편하게 개발할 수 있다.
padLeft
라는 함수를 살펴보자.
padLeft가 하는 일은 넘어온 2개의 인자 중 첫번째 인자가 number이면 해당 공간만큼 띄우고 두번 째 인자를 연결하여 반환하고, 첫번 째 인자가 string이면 해당 두 인자를 연결하여 반환한다.
위 코드는 padding 만큼 공간을 띄운 후 input을 연결하는 코드이다.
TypeScript에서는 이 코드는 컴파일 되지 않고 오류가 발생한다.
넘겨받은 padding이 number일 수 있지만 string 일 수도 있기 때문에 string이 아닌 number타입에만 적용할 수 있는 연산을 사용했기 때문이다.
이는 if문에서 typeof를 통해 narrowing을 해줄 수 있다.
if를 통해 number임을 검사했으니 TypeScript는 if문 안에서의 padding의 타입은 number라는것을 알게된다.
이렇게 TypeScript에서 타입을 줄여가는 방법은 여러가지가 있는데 하나씩 살펴보도록 하자
typeof
type guardstypeof 를 통해 검사할 수 있는 타입 종류는 다음과 같다.
위에서 살펴본 예제에서 typeof는 서로 다른 분기점에서 타입을 구분하기위해 사용되며 TypeScript는 이 typeof
를 더 잘 이해할 수 있다.
TypeScript에서 typeof
에 의해 반환되어 값이 체크되는것을 타입 가드
라고 부르고 많은 함수에서 typeof를 통해 type을 좁혀나가는 narrowing을 할 수 있다.
TypeScript에서는 &&
, ||
, if
와 같은 조건문과 !
와 같은 연산자를 통해 narrowing을 할 수 있다.
TypeScript에서는 if 와 같은 구조문은 조건 부분을 먼저 boolean 형태로 변환하고 이에 따라 분기처리된다.
위 6가지는 false 를 반환하고 그 외 모두는 true를 반환한다.
위에서 살펴보았던 padLeft를 다시 살펴보자.
padding에 | null
을 추가하여 null이 넘어올 수 있다고 가정해보자.
null을 narrowing하기위해 if(padding)
을 추가하였다.
하지만 이렇게 작성하면 잠재적인 문제가 발생할 수 있다.
위에서 if에서 반환되는 값 중 ''
빈 문자열 또한 false로 반환하고, 빈 문자열은 padLeft
함수에서
원하는대로 동작하지 않을것이다.
빈 문자열도 문자열이라는것을 생각하면 문제가 발생할 수 있는것이다.
padding !== null
검사로 수정하여 해결할 수 있다.
TypeScript에서는 switch
문과 ===
, !==
을 사용하여 equality narrowing을 처리할 수 있다.
매개변수 x는 string | number
타입을 가지고 y는 string | boolean
의 타입을 가진다.
첫 번째 if문에서 x와 y는 값이 같고 타입도 같다는것을 알게된다. 따라서 TypeScript는 if안에서의 x, y는 string type이라는 사실을 아는 것이다.
in
Operator Narrowingin
이라는 연산자를 통해 object가 특정 프로퍼티를 가지고있는지 검사할 수 있다.
매개변수 animal
은 Fish 혹은 Cat인 유니온 타입이다.
if의 조건으로 'swim' in animal
을 사용함으로써 animal에 ‘swim’ 이라는 프로퍼티가 존재하는지 확인하고, 존재한다면 Fish와 Cat중 Fish라는 사실을 알게되는것이다.
그래서 if문 밑에서는 TypeScript는 animal이 Cat임을 알고 위 예제처럼 jump라는 프로퍼티에 대해 자동완성을 할 수 있는 것이다.
instanceof
Narrowing어떤 값이 다른값의 인스턴스인지 여부를 검사하는 연산자인 instanceof
를 사용할 수 있다.
매개변수 x는 Date 혹은 string 타입이다.
if의 조건으로 값 x가 Date객체의 인스턴스인지 여부를 검사할 수 있다.
그러면 if안에서의 x는 Date타입이고, else에서의 x는 string임을 알 수 있다.
class를 살펴보면 더 자세히 설명되어있겠지만 new 연산자로 생성될 수 있는 대부분의 객체에 instanceof는 유용하게 사용할 수 있다.
어떠한 변수에 값을 할당할 때 우변의 값을 확인하고 좌변의 식별자에 대해 narrowing을 실행한다.
처음 x가 선언될 때 number | string
의 타입으로 으로 선언되었다.
따라서 아래에서 변수 x에는 number인 1을, string인 Hello를 각각 할당할 수 있다.
그런데 더 아래에서 number, string이 아닌 boolean타입을 할당하려고 하면 에러가 발생한다.
TypeScript에서는 리턴 타입에 명제 타입
을 사용함으로써 사용자 정의 타입 가드
를 정의할 수 있다.
반환값에 boolean을 반환하고 반환 조건은 animal is Fish
인지를 반환한다고 명시한다.
그리고 return에서는 animal을 Fish로 형변환하고 swim의 프로퍼티가 존재하는지 여부를 리턴하고 있다.
반환타입에 사용하는 변수명 animal은 매개변수로 받는 이름과 같아야 한다.
Fish | Cat
을 반환하는 getAnimal함수를 통해 리턴값을 반환받은 후 isFish를 통해 Fish인지 Cat인지 구분할 수 있다.
그러면 if문 내에서는 Fish라는 사실을 알 수 있고, else에서는 Cat인지를 아는것이다.
지금까지는 단순한 문자열, 숫자, 불린 타입들에 대해 Narrowing하는것을 살펴보았다.
TypeScript를 사용하다보면 이보다 더 복잡한 타입의 Narrowing하는것이 필요할 수 있다.
원과 정사각형을 구분하는 코드를 살펴보자.
kind가 가지는 값이 circle 혹은 square로 union type으로 사용되고 있고 이 값을 통해 원인지 정사각형인지 구분한 후 정사각형이라면 변의 길이인 sideLength를 사용하고,
원이라면 반지름값인 radius값을 사용할 것이다.
kind를 비교할 때 잘못된 값으로 비교하는것을 방지할 수 있다. (타이핑 오류 등)
kind의 타입으로 설정하지 않은 값으로 비교하는 경우 오류가 발생한다.
매개변수로 넘겨받는 shape가 원이라고 가정하고 위의 코드를 살펴보자.
shapre.raduis를 사용하려고 할 때 shape.raduis
는 undefined일 수 있어서 오류가 발생한다.
(tsconfig - strictNullChecks 설정을 했을 때)
shape에 대한 프로퍼티 체크를 해보자
이렇게 수정했음에도 여전히 동일한 오류가 발생한다.
그 이유 역시 위와 동일하다. shape.raduis는 undefined일 수 있다.
이는 shape.radius에 !
(non-null assertion) 을 사용함으로 써 해결할 수 있다.
shape.radius의 값이 항상 존재한다는 의미이다.
오류는 사라졌지만 여전히 맘에 들지 않는다. 어떠한 값에 대해 강제적으로 실행하도록 하였기 때문이다.
어떻게 수정해보는게 좋을까?
interface를 Circle과 Square으로 분리하였고, Shape의 타입을 Circle | Square
타입으로 선언하였다.
그리고 shape.kind
를 검사함으로써 if안에서는 Circle인지 Squire인지 알 수 있게 되었다.
이는 switch
로 사용할 수 도 있다.
never
Type & Exhaustiveness CheckingTypeScript는 타입이 존재하지 않는 상태를 표현할 때 never
을 사용할 수 있다.
절대 발생할 수 없는 타입이며 코드에서 반환할 수 없는 값이다. 어떤 의미인지 코드로 살펴보자.
위에서 getArea에 default를 추가하고 처리되지 않은 타입에 대해서는 never를 리턴하도록 했다.
그리고 Shape에 새로운 타입인 Triangle 을 추가해보자.
새로운 타입을 추가하고 getArea를 보자
새롭게 추가한 Triangle 타입은 never형식에 할당할 수 없다는 메세지가 뜬다.
즉 getArea에서 처리하지 않는 타입이 default로 넘어왔다는것을 알 수 있다.
never를 사용하여 잘못된 타입이 들어오는것을 방지할 수 있고, 오류를 통해 개발자는 triangle에 대한 case문을 추가할 수 있을것이다.
매개변수로 함수를 넘겨받는 경우 어떻게 매개변수를 표현해줄 수 있는지 살펴보자
(str: string) ⇒ void
는 문자열 타입인 str이라는 매개변수를 갖고 반환값이 없는 함수를 의미한다.
함수 선언문과 동일한 구조의 매개편수 타입을 지정하였는데, 이렇게 타입이 특정되지 않았다면 이 매개변수의 타입은 암묵적으로 any
로 결정된다.
type alias를 사용해서 함수 타입 지정을 분리할 수 있다.
다른 언어에서 함수타입에 프로퍼티를 선언하는것은 불가능하지만, JavaScript에서는 가능하다. 함수타입에 프로퍼티를 갖는 JavaScript코드를 우선 살펴보자.
desFunc
는 num을 넘겨받아 typeof를 통해 number 타입인지 반환하는 함수이다.
이 함수에 description의 프로퍼티를 지정할 수 있다.
TypeScript에서 이 코드를 어떻게 작성할 수 있을까?
Object.assign
을 통해서 TypeScript에서는 이를 구현할 수 있다. 함수 타입과 프로퍼티를 Object.assign의 매개변수로 넘겨주어 위에서 살펴봤던 JavaScript에서 함수형 타입에 프로퍼티를 갖는 변수를 만들 수 있다.
클래스를 넘겨받아 객체를 생성해주는 함수가 있다고 가정해보자.
대략 코드는 이렇게 생겼다.
createAnimal
함수는 클래스를 넘겨받아 객체를 생성한 후 리턴해준다. createAnimal 함수 내에서 매개변수로 넘겨받은 animal의 typeof를 출력해보면 ‘function’이라고 나온다.
TypeScript로 동일한 동작을 하는 코드를 작성했다.
JavaScript와 동일하게 Function
타입으로 매개변수를 받았으나 이 코드는 컴파일이 되지 않는다.
그 이유는 TypeScript에서는 new 연산자와 함께 일반 Function을 호출할 수 없기 때문이다.
이럴 땐 Construct Signature
를 사용해야 한다.
매개변수로 넘겨받는 animal은 AnimalConstructor
타입임을 명시해주고 Dog, Cat의 클래스들은 각각 Animal
을 implements 받았다.
그러면 Animal의 type대로 생성자 구조로 사용하게 된다.
매개변수로 넘겨받은 배열에서 첫번 째 요소를 리턴하는 함수이다.
리턴타입 역시 any
일 것이다. 타입을 가질 수 있다면 더 좋을 것이다.
TypeScript에서는 함수 시그니쳐 내에서 타입 매개변수를 선언함으로써 Generic을 구현할 수 있다.
함수에 Type이라는 타입 매개변수를 추가하고 이를 입력값과 반환값에도 명시한다.
함수를 호출할 때에는 이와같이 사용할 수 있다.
TypeScript는 Function overload를 지원한다.
JavaScript에는 Function overloads가 지원하지 않기 때문에 위와같이 작성했던 코드를 TypeScript에서는 여러개의 함수로 분리할 수 있다.
함수의 구조를 우선 선언하고 구현부를 작성한다.
이러한 함수의 구조를 선언한 2개를 overload signature
라고 부른다.
위 예제로 생성된 함수는 2개 혹은 3개의 매개변수만을 받을 수 있고 1개의 매개변수로는 호출할 수 없다.
매개변수가 같고 타입이 다른 경우에는 Union 타입을 사용해준다.
Function overload의 주의사항
overload signature
와 구현부는 호환되어야 한다.만일 이와같이 string 혹은 배열을 넘겨받아 length을 계산해주는 함수를 overload하여 작성했다고 생각해보자.
이러한 두 개의 시그니쳐는 같은 매개변수 개수와 같은 리턴타입을 가지기 때문에 overload가 없는 형태로 작성하는것이 더 보기 좋다.
매개젼수를 Destructuring을 통해 해체할 때에도 타입을 지정해줄 수 있다.
이와같이 타입을 지정해줄 수 있고,
type alias를 통해 상단에 타입을 선언할 수 있다.
모든 타입의 최 상단 레벨에 존재하는 unknown과 any
unknown과 any는 모든 타입을 할당할 수 있는 비슷한 성격을 가진다.
차이점은 프로퍼티를 사용할 때 나타나는데, any는 하위 프로퍼티 사용에 제약이 없는 반면 unknown은 타입이 정해져있지 않은 상태에서 하위 프로퍼티에 접근하려하면 컴파일 단계에서 오류가 발생한다.
이는 형변환을 통하여 정확한 타입을 유추한 후 사용할 수 있다.
모든 타입의 최 하단 레벨에 존재하는 never
최 하단에 존재하기 때문에 어떠한 값도 never타입의 변수에는 할당할 수 없다.
코드가 도달할 수 없는 지점에 해당 타입에 값을 할당하는 코드를 작성해두면,
추후 도달할 수 없는 지점에 구멍이 생겨 코드가 도달할 수 있는 가능성이 생겼을 때 TypeScript는 컴파일 오류가 발생하고, 개발자는 이를 확인할s 수 있다.