관리 메뉴

꿈꾸는 개발자

9장 타입 제한자 본문

Learning Typescript

9장 타입 제한자

rickysin 2023. 3. 21. 21:10

9.1 top 타입


  • top 타입: 가능한 모든 값을 나타내는 타입 ⇒ 모든 타입은 top에 할당할 수 있다.

9.1.1 any 다시 보기


  • any 타입 top 타입과 유사함(모든 타입의 위치에서 제공 가능)
let value:any; 
value="string";//ok
value=123;//ok
console.log(value);
  • 하지만, any의 경우 타입 검사를 수행하지 않음 (타입스크립트의 유용성이 떨어짐)
  • unknow으로 선언하는 것이 더 안전함

9.1.2 unknown


  • TS에서 unknown 타입은 진정한 top 타입임
  • any와의 공통점/차이점
    • ****************공통점:****************모든 타입을 unknown타입 위치로 전달 가능(any와 유사함)
    • 차이점: TS는 unknown 타입의 값을 더 제한적으로 취급함
  • TS는 unknown 타입 값의 속성에 직접 접근X
  • unknown 타입은 top 타입이 아닌 타입에는 할당X
function greet(name:unknown){
    console.log(`${name.toUpperCase()}`);//name 속성에 접근하고자 하면 에러 발생!
}
  • unknown 타입인 name에 접근할 수 있는 유일한 방법은 값의 타입이 제한된 경우
function greet(name:unknown){
    if(typeof name==="string"){
        console.log(`${name.toUpperCase()}`);
    }else{
        console.log(`well, I'm off.`);
    }
}

greet("Beey");
greet({}); //로그 없음?
  • 위와 같은 이유들로 unknown이 any보다 훨씬 안전하다.

9.2 타입 서술어


function A(value:unknown){
    return['number','string'].includes(typeof value); //boolean값을 반환함
}
function B(value:number|string|null|undefined){
    if(A(value)){ //true값이기 때문에 number/string 중 하나라고 생각하겠지만
									//TS는 오직 boolean이란 사실 밖에 인지하지 못함!
        value.toString(); //Error:Object is possibly undefined. 
    }else{
        console.log("does not exist:",value);
    }
}
  • TS는 위의 과정이 인수 좁히기란 것을 인지하지 못함!
  • 타입 서술어(type predicate) ⇒ 사용자 정의 타입 가드: 인수가 특정 타입인지 여부를 확인하기 위해 boolean을 return 하는 함수를 위한 것!(instanceof/typeof를 통해 자체적인 타입 가드를 생성한다)
    • 목적: 매개변수로 전달된 인수가 매개변수의 타입보다 더 구체적인 타입인지 확인
function typePredicate(input:WideType):input is NarrowType
  • 위와 같이 사용 가능! ⇒ value 코드에서 is를 사용해 사용자 정의 타입 가드를 지정할 경우 밑과 같이 코드를 작성할 수 있다.
function A(value:unknown):value is number|string{
    return['number','string'].includes(typeof value); //boolean값을 반환함
}
function B(value:number|string|null|undefined){
    if(A(value)){ 
				// value: number | string 타입
        value.toString(); //Error가 사라짐!
    }else{
				//null | undefined
        console.log("does not exist:",value);
    }
}
  • 위와 같을 경우 TS는 value가 string | number일 경우 ⇒ string | number으로 추론한다.
    • 필자 피셜 is의 용도: 보통 한 인터페이스의 인스턴스로 알려진 객체가 더 구체적인 인터페이스의 인스턴스 여부를 검사하기 위해 주로 사용한다고 함!
interface A{
    funny:boolean;
}
interface B extends A{
    routine:string; 
}
//더 구체적인 interface를 명시함 =>인터페이스의 경우 범위의 확장
//let b:B로 타입 애너테이션이 된 경우는 A/B 둘의 인스턴스로 생성이 가능하다!
function C(value:A): value is B{
    return "routine" in value
}   
function D(value:A){
    if(C(value)){
        //value의 타입 => B에 해당함!
        console.log(value.routine);
    }
    console.log(value.routine); //Error 발생 type A로 간주 routine 속성 존재X
}
  • 하지만 타입 서술어는 false 조건에서 타입을 좁히기 때문에 주의가 필요!
function A(input:string|undefined):input is string{
    return !!(input&&input.length>=7);
}

function B(text:string|undefined){
    if(A(text)){
        //text:string 타입
        console.log(`${text}`);
    }else{
        //text는 undefined;
        console.log(`${text?.length}`);//Error length does not exists on type 'never'
    }
}
  • 즉 입력된 타입 이상(여기에서 말하는 이상은 타입 외 추가적인 내용을 검색하고자 할 때를 의미하는 듯) ⇒ 즉, 위 예시의 경우 string타입을 확인하고자 하는 목적 외에도 string의 길이를 통해 좁히기를 시도하려고 했음 ⇒ 이럴 경우 string임에도 불구하고, 단지 길이가 7미만이란 이유로 TS는 이를 undefined으로 간주할 수 있다는 의미!

9.3 타입 연산자


9.3.1 keyof


interface A{
    aud:number;
    cri: number;
}
function B(rate:A,key:string):number{
    return rate[key]; //Error => string 형식의 인덱스 접근이 허용되지 않음!
}

const rating:A={aud:66,cri:84};
B(rating,'aud');// 허용됨!
B(rating,"not valid") //허용되만 사용하면 안됨!
  • type string은 A 인터페이스에서 속성으로 허용되지 않는 값을 허용?
    • string이란 넓은 범주가 오류의 가능성을 내포하기 있기 때문에?
  • A는 string 키를 허용하는 인덱스 시그니처를 선언하지 않음
    • 말 그대로, aud/cri 외 다른 string 타입의 문자열이 들어왔을 때 허용하지 않았음에도 밑 코드는 그것을 허용하고 있는 것을 보여줌!(B(rating, “not valid”)의 예시가 잘 보여줌)
interface A{
    aud:number;
    cri: number;
}
function B(rate:A,key:string):number{
    return rate[key]; //Error => string 형식의 인덱스 접근이 허용되지 않음!
}

const rating:A={aud:66,cri:84};
B(rating,'aud');// 허용됨!
B(rating,"not valid") //허용되지만 사용하면 안됨!

function C(rate:A,key:"aud"|"cri"):number{
    return rate[key]; //리터럴 값으로 유니언 타입을 형성했을 때는 허용한다.
    //컨테이너에 존재하는 key만을 적절하게 제한하는 것이 중요함!
}

const ratings:A={aud:55,cri:85};

C(rating,"aud"); //ok
C(rating,"not valid") //에러 발생

위는 keyof를 사용하지 않으면 발생하는 불편한 점을 보여주기 위해 있는 것들 이제부터 핵심:

  • C와 같은 방법은 속성이 많아지면 하기 힘들어짐
    • keyof를 사용해라 (기존에 존재하는 타입 + 타입에 허용되는 모든 키의 조합을 반환함)
interface A{
    aud:number;
    cri: number;
}
function B(rate:A,key: keyof A):number{
    return rate[key]; //ok
}

const rating:A={aud:45,cri:55};

B(rating,"aud");//ok
B(rating,"not valid"); //Error:keyof A에 할당 불가능하다고 나옴!
  • 위 문제를 간결하게 해결함, 매개변수에 유니언 타입을 선언할 필요도 없음! ⇒ (keyof가 생성해줌)

9.3.2 typeof


  • typeof: 제공되는 값의 타입을 반환함
const original={
    medi:"movie",
    title:"mean",
};
let adaptatio: typeof original;
if(Math.random()>0.5){
    adaptatio={...original,medi:"play"} //ok
}else{
    adaptatio={...original,medi:2}; //Error: number is not assignable to string
}
  • JS typeof vs TS typeof
    • JS typeof: 타입에 대한 문자열 이름을 반환하는 런타임 연산자
    • TS typeof: 제공된 값의 타입 반환(TS에서만 사용 가능)

<aside> 💡 keyof/typeof: 값의 타입 검색/값에 허용된 키 검색

</aside>

const ratings={
    imdb:8.4,
    metacri:82,
}
//키가 rating 값 타입의 키 중 하나임을 나타내야 함! 
function logRate(key: keyof typeof ratings){
}

logRate("imdb"); //ok

logRate("invalid")// Argument of type '"invalid"' is not assignable to parameter of type '"imdb" | "metacri
  • keyof + typeof를 혼합해서 사용하면 ⇒ 명시적 인터페이스가 없는 객체에 허용된 키들의 타입의 코드를 작성하느라 수고할 필요X

9.4 타입 어서션


  • 강력하게 타입화(strongly typed)될 때 TS코드는 잘 작동함 (모든 값이 정확히 알려진 타입의 경우를 의미함)
    • 경우에 따라서, 코드 작동을 알려주지 못할 수도 있음
      • ex) JSON.parse는 의도적으로 top 타입인 any를 반환함 ⇒ JSON.parse가 특정 문자열이 주어졌을 때 특정한 값 타입을 반환해야 한다고 안전하게 알려줄 방법은 없음! (제네릭의 황금률을 위반하기 때문에!)
  • 타입 어서션 or Type cast: 값의 타입을 재정의 (타입 다음에 as 키워드 추가)
const rawData=["a","b"];

//타입 any 
JSON.parse(rawData);//Error 발생 stirng[] => string에 할당 불가!

//타입 string[];
JSON.parse(rawData) as string[];
  • 교재에선 위 예시들을 제시하지만, 애초에 JSON.parse()의 인수로 string[]을 전달할 수 없는 것으로 보인다.
const rawData="hello";

//타입 any 
JSON.parse(rawData);//Error 발생 stirng[] => string에 할당 불가!

//타입 string[];
JSON.parse(rawData) as string[];

//타입 [string,string];
JSON.parse(rawData) as [string, string];
  • 오히려 일반 문자열 리터럴을 할당했을 때 에러 발생X ⇒그리고 위와 같이 해도 as를 사용하는 목적에 위배되지 않는 것으로 보인다. 애초에 as를 사용하지 않으면 JSON.parse()는 any를 반환하기 때문에!
  • 흠….필자에 따르면 애초에 TS의 모범 사례는 타입 어서션을 사용하지 않는 것이라고 한다 (코드의 완전한 타입화를 지향함)⇒ BUT, 종종 유용하기도 함!

9.4.1 포착된 오류 타입 어서션


오류를 처리할 때 타입 어서션이 매우 유용할 수 있음 ⇒ try 블록에서 예상과 다른 객체를 발생 가능? ⇒ catch()에서 오류의 타입을 알기 어려움

  • 만약 Error 클래스의 인스턴스가 발생될거라고 확신을 한다면, 타입 어서션을 사용해 어서션 오류 처리 가능
try{
    //오류를 발생시키는 코드
}catch(error){
//Error 클래스의 인스턴스라고 가정하고, error의 message 속성에 접근!
    console.warn((error as Error).message);
}
  • instanceof와 같은 타입 내로링을 사용하는 것이 더 안전할 수 있음!
try{
    //오류를 발생시키는 코드
}catch(error){
    console.warn(error instanceof Error? error.message:error);
}

9.4.2 non-null 어서션


  • null+undefined을 포함할 수 있는 변수에서 null과 undefined를 제거할 때 타입 어셔선을 주로 사용함 ⇒ 예약어를 제공함(!)
let a=Math.random()>0.5? undefined:new Date();

//타입이 Date라고 간주됨!
a as Date;
//타입이 Date라고 간주됨(undefined를 제거)
a!
const a=new Map([["a","a"],["b","b"],])

//type:string|undefined;
const maybeValue=a.get("a"); 

console.log(maybeValue.toUpperCase());//Error: maybeValue' is possibly 'undefined

//type:stirng으로 고정됨?
const knowValue=a.get("a")!; // !을 사용함으로 undefined이 제외됨!
console.log(knowValue.toUpperCase());//ok undefined이 제거됨!
  • 예시에서는[”a”,1]이런 형태인데 이러면 get을 사용하면 number타입 아닌가? 위 코드는 교재에 있는 코드를 임의로 변경한 것 그래야 두 번째 console.log()에서 에러 발생X

9.4.3 타입 어서션 주의 사항


  • any 타입과 마찬가지로 가능한 사용 자제를 해야 함!
const a = new Map([["a", "a"], ["b", "b"],]);
const b = a.get("a")!;
console.log(typeof b);//타입은 string임
console.log(b.toUpperCase());//타입 오류는 아니지만, runtime에서 오류가 발생함?
  • 실제 위의 코드를 브라우저 개발자 환경에서 실행했을 때 문제없이작동하지만, 교재에서는 Map의 값이 변경되는 경우를 상정했기 때문에 타입이 변경되면 toUpperCase()메서드가 문제될 수 있음!

어서션 vs 선언

변수 타입을 선언하기 위해 타입 애너테이션 사용 VS 변수 타입을 변경하기 위해 타입 어서션을 사용!

  • 타입 선언을 위한 타입 애너테이션: 타입 검사기는 초깃값에 대한 할당 가능성을 검사함
  • 타입 어서션: TS에 타입 검사 중 일부를 건너뛰도록 명시!
interface A{
    acts:string[]; 
    name:string;
}

const declared: A={
    //Property 'acts' is missing in type '{ name: string; }' 
    //but required in type 'A'
    name:"aa",
}
const asserted={
    name:"bbb" //여기에선 오류가 발생X
}as A

//아래 코도는 런타임 에러로 인해 정상 작동X 
console.log(declared.act.join(", "));
console.log(asserted.act.join(", "))
  • 따라서, 처음부터 타입 애너테이션 or TS가 초깃값에서 변수의 타입을 유추할 수 있도록 하는 것이 좋음!

어서션 할당 가능성

  • 타입 어서션은 둘 중 하나의 타입에 할당 가능한 경우 허용됨!
    • 서로 전혀 관련없을 경우 타입 오류 감지함!
let myValue="aaa" as number; //Error 발생 겹치지 않기 때문에!
  • 완전히 관련없는 타입으로 전환해야 하는 경우 이중 타입 어서션을 사용함 ⇒ 값을 any/unknown 같은 top 타입으로 전환 후 그 결과를 관련없는 타입으로 전환함!
let myValueDouble="1234" as unknown as number;
  • 필자는 위의 방법은 안전하지 않고, 코드 문제가 쉽지 발생할 수 있다고 경고함!

9.5 const 어서션


  • const 어서션은 모든 값(배열, 원시 타입, 값 등등) 상수로 취급할 때 사용함
    • 배열은 가변 배열이 아니라 읽기 전용 튜플로 취급
    • 리터럴은 ≠일반 원시 타입 ⇒ 리터럴로 취급
    • 객체 속성은 읽기 전용으로 간주
    //타입:(number | string)[];
    [0," "];
    
    //타입:readonly [0," "];
    [0," "] as const;
    

9.5.1 리터럴에서 원시 타입으로


  • 특정 리터럴을 반환하는 함수에 사용하면 적절!
const getName=()=>"Maria Bam";//()=>string;

const getNameConst=()=>"Maria" as const;//()=>"Maria"
interface A{
    quo:string;
    sty: "story"|"one-liner";
}

function tellJoke(joke:A){
    if(joke.sty==="story"){
        console.log(`${joke.quo}`);
    }else{
        console.log(joke.quo.split("\\n"));
    }
}

const narrowJ={
    quo:"hello world",
    sty:"one-liner" as const,
}

tellJoke(narrowJ) //ok

const wideObject={
    quo:"heelo world",
    sty:"one-liner",
}
tellJoke(wideObject);//Error:Type 'string' is not assignable to type '"story" | "one-liner"'
  • 위처럼 값의 특정 필드가 더 구체적인 리터럴 값을 갖도록 사용 가능!

9.5.2 읽기 전용 객체


function des(preference:"maybe"|"no"|"yes"){
    switch(preference){
        case "maybe":
        return "I supp";
        case "no": 
        return "no"
        case "yes":
        return "yes"
    }
}
const preferencesMutable={
    movie:"maybe",
    standup:"yes",
}
//Error:stirng is not assignable to "maybe"|"no"|"yes"
des(preferencesMutable.movie);
preferencesMutable.movie="no"//ok
const preferenceReadonly={
    movie:"maybe",
    standup:"yes",
}as const;
des(preferenceReadonly.movie);//ok
preferenceReadonly.movie="no";
//Error: cannot assign to "movie" because it is a read-only property
  • 재귀적으로 const 어서션이 적용된다 ⇒ 즉 중첩 객체에도 적용이 된다는 의미임!

Reference:

조시 골드버그, 러닝 타입스크립트, 고승원 옮김, 2023