typescript

타입스크립트 2.13 ~ 2.17

케케_ 2024. 9. 28. 18:45

목차

 


2.13 객체 간에 대입할 수 있는지 확인하는 법을 배우자

: 어떤 객체가 더 넓은(추상적인)가만 확인하자 

-> 좁은 객체가 넓은 객체에 대입 가능

 

잉여 속성 검사가 이루어 지지 않는 변수를 대입할 때, 가능 여부를 따져봐야함

 

interface A {
    name : string;
}

interface B {
    name : string;
    age : number;
}

const aObj = {
    name : 'zero'
}

const bObj = {
    name : 'nero',
    age : 32,
}

const aToA : A = aObj;
const bToA : A = bObj;
const aToB : B = aObj;
const bToB : B = bObj;

aToB에만 오류

  • A타입이 B타입보다 넓은, 추상적인 타입
    • B타입은 더 좁은, 구체적인 타입 : name과 age 속성이 꼭 있어야함 

 

 - 집합 관계로 보면,

  • {name:string} & {age:number} -> 교집합 관계에 따라 {name:string, age : number}

 

합집합은? {name:string} | {age:number} 이면

  • {name:string, age : number} 또는 {name:string} 또는 {age:number}에 대입 가능? --> 다 불가능
  • 합집합은 각각의 집합이나 교집합보다 넓기 때문
interface A {
    name : string;
}

interface B {
    age : number;
}

function test() : A|B {
    if (Math.random() > 0.5){
        return {
            age : 28,
        }
    }
    return {
        name : 'zero'
    }
}

const target1 : A & B = test()
const target2 : A = test()
const target3 : B = test()
  • 아래의 3줄의 코드 모두에서 에러
    • 더 좁은 타입의 변수에 합집합 대입 시도

 

배열튜플

let a : ['hi', 'readonly'] = ['hi', 'readonly'];
let b : string[] = ['hi', 'normal'];

a=b;
b=a;

  • 튜플 < 배열
  • 따라서 튜플 a는 배열 b에 대입 가능
  • 반대는 위와 같이 오류

 

 

배열과 튜플에 readonly가 붙으면?

let a : readonly string[] = ['hi', 'readonly'];
let b : string[] = ['hi', 'normal'];

a=b;
b=a;

  • readonly가 붙은 배열은 더 넓은 타입
  • 따라서 readonly가 붙어 넓어진 a가 일반 배열 b에 들어갈 수 없음

 

readonly 튜플과 일반 배열은?

let a : readonly ['hi', 'readonly'] = ['hi', 'readonly'];
let b : string[] = ['hi', 'normal'];

a=b;
b=a;

  • a = b --> 배열이 튜플보다 넓은 타입이므로 에러
  • b = a --> 위처럼 튜플이 배열보다 좁은 타입이긴하지만, readonly가 붙으면 일반 배열보다 넓은 타입

--> 두 경우 모두에서 에러

 

 

속성이 동일한 두 객체, 하지만 한 객체는 옵셔널인 경우? -> 옵셔널 객체가 더 넓은 객체

type Optional = {
    a? : string;
    b? : string;
};

type Mandatory = {
    a : string;
    b:string;
};
const o : Optional = {
    a : 'hello',
};
const m : Mandatory = {
    a:'hello',
    b:'world',
};

const o2 : Optional = m
const m2 : Mandatory = o

  • 옵셔널 : 기존 타입에 undefined가 유니언된 것
  • 일반 객체에 옵셔널 객테를 대입할 경우 에러 확인

 

배열과 다르게 객체readonly는 서로 대입 가능

type ReadOnly = {
    readonly a : string;
    readonly b : string;
};

type Mandatory = {
    a : string;
    b:string;
};
const o : ReadOnly = {
    a : 'hello',
    b:'world',
};
const m : Mandatory = {
    a:'hello',
    b:'world',
};

const o2 : ReadOnly = m
const m2 : Mandatory = o
  • 위와 같이 해도 오류 없음

 

 

 


2.13.1 구조적 타이핑 (structural typing)

: 객체를 어떻게 만들었든(인터페이스의 이름이 달라도), 구조가 같으면 같은 객체로 인식하는 것

 

interface Money {
    amount : number;
    unit : string;
}

interface Liter {
    amount : number;
    unit : string;
}

const liter : Liter = { amount : 1, unit : 'liter'};
const circle : Money = liter;
  • 두 인터페이스는 이름을 제외하고 모두 같음 -> 같은 타입으로 인식

 

2.13의 첫번째 코드도 구조적 타이핑임

interface A {
    name : string;
}

interface B {
    name : string;
    age : number;
}

const aObj = {
    name : 'zero'
}

const bObj = {
    name : 'nero',
    age : 32,
}

const aToA : A = aObj;
const bToA : A = bObj;
const aToB : B = aObj;
const bToB : B = bObj;
  • 인터페이스 B는 A의 모든 조건을 충족 
    • B는 구조적 타이핑 관점에서 A 인터페이스임
  • 반대의 경우 A는 age 속성이 없으므로 B가 아님

즉, 완전히 구조가 같아야 동일한 것 아님, B가 A라고 해서 A가 B인 것도 아님

 

 

매핑된 객체 타입의 경우에도 구조적 타이핑 특성 O

type Arr = number[];
type CopyArr = {
    [key in keyof Arr] : Arr[key];
}

const copyArr : CopyArr = [1,3,9];
  • CopyArr은 객체 타입임에도 숫자 배열이 대입 가능 = CopyArr 타입에 존재하는 속성들을 숫자 배열이 갖고 있다! -> 구조적 동일

 

더 간단한 코드 예

type SimpleArr = { [key : number] : number, length : number};
const simpleArr : SimpleArr = [1,2,3]
  • 숫자 배열은 SimpleArr 객체 타입에 속성을 갖고 있음
  • 숫자 배열은 구조적으로 SimpleArr 

 

서로 대입하지 못하게 하는 방법 (구분하는 방법)

interface Money {
    __type : 'money';
    amount : number;
    unit : string;
}

interface Liter {
    __type : 'liter';
    amount : number;
    unit : string;
}

const liter : Liter = {amount : 1, unit:'liter', __type: 'liter'}
const circle : Money = liter;

  • __type과 같이 구별할 수 있는 속성 추가
    • 속성 이름이 꼭 __type이 아니어도 됨
  • __type같은 속성을 브랜드 속성이라함
  • 브랜딩 : 브랜드 속성을 사용하는 것

 

 

 

 


2.14 제네릭으로 타입을 함수처럼 사용하자

: <>을 사용해 타입 간의 중복을 제거

 

 

js에서 함수를 사용해 중복을 제거한 예시

const personFactory = (name, age) ({
    type : 'human',
    race : 'yellow',
    name,
    age,
})
const person1 = personFactory('zero', 28);
const person2 = personFactory('nero', 32);
  • js의 함수처럼 타입스크립트에서도 중복을 제거할 수 있음

 

interface Person<N, A> {
    type: 'human',
    race : 'yellow',
    name : N,
    age : A,
}
interface Zero extends Person<'zero',28>{}
interface Nero extends Person<'nero',32>{}
  • 제네릭 표기 : <>
    • <> 안에 타입 매개변수(type parameter) 넣음 
  • 선언한 제네릭 사용 :  Person<'zero',28>처럼 매개변수에 대응하는 실제 타입 인수(type argument) 삽입

 

 

배열 선언 : Array<string>

  • Array의 타입이 아래 코트 꼴로 선언돼 있기 때문에 Array<string> 형태로 선언 가능
interface Array<T> {
    [key : number] : T,
    length : number,
    //기타 속성들
}

 

 

 

타입 매개변수의 개수 = 타입 인수의 개수

  • 일치하지 않는 경우 에러

 

 

 

인터페이스뿐만 아니라 클래스타입 별칭, 함수도 제네릭 가능

 

  • 타입 별칭
type Person<N, A> {
    type: 'human',
    race : 'yellow',
    name : N,
    age : A,
}

type Zero = Person<'zero', 28>;
type Nero = Person<'nero', 28>;

 

  • 클래스
class Person<N, A> {
    name: N;
    age : A;
    constructor (name: N,age : A){
        this.name = name;
        this.age = age
    }
   
}

 

  • 함수
    • 선언문이나 표현식이냐에 따라 제네릭 표기 위치가 다름
const personFactoryE = <N, A> (name: N,age : A) => (
    {type : 'human',
    race : ' yellow',
    name,
    age,}
);
    
function personFactoryD<N, A> (name: N,age : A) {
    return ({type : 'human',
    race : ' yellow',
    name,
    age,})
};

 

 

type과 interface 간에 교차 사용도 가능

interface IPerson<N, A> {
    type : 'human',
    race : ' yellow',
    name : N,
    age : A,
}

type TPerson<N, A>= {
    type : 'human',
    race : ' yellow',
    name : N,
    age : A,
}
    
type zero = IPerson<'zero', 28>;
interface Nero extends TPerson<'nero', 32>{}

 

 

 

타입 매개변수에 기본값(default) 넣어주기

interface Person<N=string, A=number> {
    type : 'human',
    race : ' yellow',
    name : N,
    age : A,
}
 
type Person1 = Person;
type Person2 = Person<number>;
type Person3 = Person<number, boolean>;

 

 

 

추론으로 타입을 알 수도 있음

interface Person<N=string, A=number> {
    type : 'human',
    race : 'yellow',
    name : N,
    age : A,
}

const personFactoryE = <N,A = unknown>(name: N, age : A) : Person<N,A> => ({
    type : 'human',
    race : 'yellow',
    name,
    age,
});

const zero = personFactoryE('zero', 28);

  • 넣은 인수의 타입으로 추측

--> 실제로 직접 넣지 않는 경우가 더 많음

 

 

상수 타입 매개변수

function values<T>(inital : T[]) {
    return {
        hasValue(value : T) {return inital.includes(value)}
    };
}

const savedValues = values(["a","b","c"])
savedValues.hasValue("x");

  • T = ["a","b","c"]
    • ["a","b","c"]는 string[]이기 때문에 T는 string
    • "x" 전달 가능 이유

 

상수 타입을 이용해 "a"|"b"|"c" 같이 요소의 유니온으로 추론되게 만들기

function values<const T>(inital : T[]) {
    return {
        hasValue(value : T) {return inital.includes(value)}
    };
}

const savedValues = values(["a","b","c"])
savedValues.hasValue("x");

  • 타입 매개변수 앞에 const 추가

 

 


2.14.1 제네릭에 제약 걸기

: extents 문법으로 사용 (상속과 사용법이 다름)

 

interface Example <A extends number, B = string>{
    a: A,
    b: B,
}

type Usecase1 = Example<string, boolean>;
type Usecase2 = Example<1, boolean>;
type Usecase3 = Example<number>;

  • 타입 매개변수 A는 숫자 타입이어야 한다는 의미
  • 따라서 string을 넣으면 오류
  • number보다 구체적인 1 리터럴 타입
    • 제약보가 더 구체적인 타입은 입력 가능

 

하나의 타입 매개변수가 다른 타입 매개변수의 제약이 될 수 있음

interface Example <A , B extends A>{
    a: A,
    b: B,
}

type Usecase1 = Example<string, number>;
type Usecase2 = Example<string, 'hello'>;
type Usecase3 = Example<number, 123>;

  • A의 타입에 따라 B의 타입이 구체적으로 정해짐

 

타입 매개변수 != 제약

interface V0 {
    value : any;
}

const returnV0 = <T extends V0>() : T => {
    return {value : 'test'};
}

  • T는 정확히 V0가 아님
    • T는 V0에 대입할 수 있는 모드 타입
  • 따라서 예를 들어 {value : string, another: string}과 같은 좁은 범위의 타입도 T가 될 수 있음

 

function onlyBoolean< T extends boolean> (arg : T = false) : T {
    return arg;
}

  • T가 never도 될 수있기 때문에 에러

 

해결 : 제네릭 제거

function onlyBoolean (arg : true | false = true) : true | false {
    return arg;
}

 


2.15 조건문과 비슷한 컨디션널 타입이 있다

: 조건에 따라 다른 타입이 되는 타입(conditional type)

type A1 = string;
type B1 = A1 extends string ? number : boolean; // type B1  = number

type A2 = number;
type B2 = A2 extends string ? number : boolean; // type B2 = boolean
  • express는 삼항연산자(?)와 같이 사용
  • 특정 타입이 다른 타입의 부분집합일 때 참

 

명시적으로 extends하지 않아도 됨

interface X {
    x : number
}

interface XY {
    x : number;
    y : number;
}

interface YX extends X {
    y : number
}

type A = XY extends X ? string : number;    //type A = string
type B = YX extends X ? string : number;    //type B = string
  • XY는 명시적으로 extends하지 않았음에도 A도 string
    • XY 타입이 X에 대입 가능하기 때문

 

타입 검사로 이용

type Result1 = 'hi' extends string ? true : false; //type Result1 = true
type Result2 = [1] extends [string] ? true : false; //type Result2 = false
  • [1] 은 [number] or number[]

 

never와 함께 사용

type Start =string | number;
type New = Start extends string | number? Start[] : never;  //Start[]
let n: New = ['hi'];
n = [123];
  • 위 코는 그냥 그냥 type New = Start[]라 하면됨

- 보통은 제네릭과 쓸 때 never가 의미 있음

type ChooseArray<A> = A extends string ? string[] : never;
type StringArray = ChooseArray<string>; //string[]
type Never = ChooseArray<number>;       //never

 

 

- never은 모든 타입 대입 가능 -> 모든 타입을 extends 가능

type Result = never extends string ? true : false;	//true

 

- 키가 never이면 제거됨 -> 컨디셔널 타입과 같이 사용

type OmitByType<O,T> = {
    [K in keyof O as O[K] extends T? never:K] : O[K];
}; 
type Result = OmitByType<{
    name : string,
    age : number,
    married : boolean,
    rich : boolean,
}, boolean>

  • OmitByType 타입 : 특정 타입인 속성을 제거하는 타입
    • 예제 -> 불린타입 속성 제거
    • 키가 never이면 해당 속성은 제거됨

 

중첩

type ChooseArray<A> = A extends string ? string[] : A extends boolean ? boolean[] : never;
type StringArray = ChooseArray<string>;

type BooleanArray = ChooseArray<boolean>    //boolean[]
type Never = ChooseArray<number>            //never

 

 

인덱스 접근 타입으로 컨디셔널 표현하기

type A1 = string;
type B1 = A1 extends string ? number : boolean; // number
type B2 = {                                     // number
    't' : number;
    'f' : boolean;
} [A1 extends string ? 't' : 'f']

 

 

2.15.1 컨디셔널 타입 분배법칙

 

- string | number 타입이 있는데, string[]을 얻고 싶은 경우 => 컨디셔널 + 제네릭

type Start = string | number;
type Result<Key> = Key extends string ? Key[] : never;
let n : Result<Start> = ['hi'];     //string[]
  • 검사하려는 타입이 제네릭이면서 유니언이면 분배법칙 실행
  • Result<string | number> = Result<string> | Result<number>
  • string[] | never -> string[]

 

(주의) 불린 타입과 사용할 경우

type Start = string | number | boolean;
type Result<Key> = Key extends string|boolean ? Key[] : never;
let n : Result<Start> = ['hi'];     //string[] | false[] | true[]
n=[true]
  • boolean을  false or true로 인식

 

분배법칙 막기

type IsString<T> = T extends string? true : false;
type Result = IsString<'hi' | 3>        // boolean
  • false 아닌 boolean이 나옴 <- 분배법칙 때문
type IsString<T> = [T] extends [string]? true : false;
type Result = IsString<'hi' | 3>        // false
  • 배열로 제네릭 감싸면 분배법칙 X

 

never도 분배법칙 대상

 - 유니언(아니지만)으로 생각하는게 좋음

type R<T> = T extends string ? true : false;
type RR = R<never>        // never
  • never가 되면서 분배법칙
  • never는 공집합 -> 공집합에 분배법칙 ? 아무것도 실행하지 않는 것 => 결과 never
type IsNever<T> = [T] extends string ? true : false;
type T = IsNever<never>             // true
type F = IsNever<'never'>           // false
  • []로 막아서 해결

 

제네릭과 컨디셔널을 같이 사용할 경우 조심할 사항

function test<T>(a:T) {
    type R<T> = T extends string? T: T;
    const b: R<T> = a;  //오류 : Type 'T' is not assignable to type 'R<T>'. 대입불가
}
  • 예상 : R<T>는 T타입이 될것 -> b는 R<T>니까 얘도 T-> 그럼 a 대입 가능
  • 문제 : R<T>가 T타입이 될거란 생각
    • 제네릭이 들어있는 컨디셔널 타입을 판단할 때 값을 판단을 뒤로 미룸
    • 변수 b에 매개변수 a 대입할때까지 판단 X

 - 해결 :  배열로 감싸기

function test<T extends ([T] extends [string] ? string : never)>(a:T) {
    type R<T> = [T] extends string? T: T;
    const b: R<T> = a;  //오류 : Type 'T' is not assignable to type 'R<T>'.
}

 

 

2.16 함수와 메서드를 타이핑하자

 

매개변수에 옵셔널

function example(a:string, b?: number, c = false) {}

example('hi', 123, true);
example('hi', 123);
example('hi');

  • 기본값이 제공된 경우 자동으로 옵셔널

 

...문법 : 나머지 매개변수 문법

function example1(a: string, ...b: number[]){}
example1('hi',123,4,56)     //b=[123,4,56]
function example2(...a: string[], b: number)
  • example2 오류
    • 매개변수의 마지막자리에만 ...문법 위치

 

매개변수 자리에 전개문법

function example3(...args : [number, string, boolean]){}
example3(1,'123',false)    
function example4(...args : [a : number, b: string, c: boolean]) {}

  • example3 매개변수의 이름을 자동 할당
  • example4 이름을 직접 정한 방법

 

구조분해 할당

function destructuring ({prop : {nested}}) {}
destructuring ({prop : {nested : 'hi'}})

  • 에러 발생으로 타이핑 필요
function destructuring ({prop : {nested : string}}) {}
destructuring ({prop : {nested : 'hi'}})

  • 이렇게 하면 타입 표기한게 아닌 nested 속성을 string 변수로 이름을 바꾼 것
function destructuring ({prop : {nested}} : {prop : {nested : string}}) {}
destructuring ({prop : {nested : 'hi'}})
  • 위와 같이 표기

 

this를 사용하는 경우 -> 명시적 표기 / 안하면 any 추론 및 에러 발생

function example1(){
    console.log(this)   //any 타입 에러
}

function example2(this : Window) {
    console.log(this)       //this:Window
}

function example3(this : Document, a : string, b:'this'){}
example3('hello', 'this')   //this가 Document 타입일 수 없음

example3.call(document, 'hello', 'this')
  • this는 매개변수의 첫번째 자리에 -> 다른 매개변수는 한 자리씩 밀림
  • this는 실제 매개변수가 아님
  • call 메서드 등을 이용해 this 값을 명시적으로 지정

 

메서드에서 this 사용

  • this가 바뀔 수 있을 때 명시적으로 타이핑
type Animal = {
    age : number;
    type : 'dog'
};
const person = {
    name: 'zero',
    age : 28,
    sayName() {
        this;
        this.name;      //(property) name : string
    },
    sayAge(this: Animal) {
        this;           //this : Animal
        this.type;      //(property) type : "dog"
    }
};
person.sayAge.bind({age : 3,type : 'dog' })

 

 

타입 스크립트에서는 함수를 생성자로 사용 불가/ 대신 class 사용

  • 강제로 만들 수 있지만, 부자연스러운 방법 (굳이 정리 X)

 

 

 

 

2.17 같은 이름의 함수를 여러 번 선언할 수 있다

 

두 문자를 합치거나 두 숫자를 더하는 함수

function add (x: string | number,y: string | number) : string| number {
    return x + y
}

add (1,2)   //3
add('1','2')//12
add(1, '2') //12 원하지 않는데 됨
add('1', 2) //12 원하지 않는데 됨

  • x+y 오류
  • 다른 타입 간의 +는 원하지 않는데 됨

(해결) 오버로딩 : 호출할 수 있는 함수의 타입을 미리 여러 개 타이핑하는 기법

function add (x:number,y: number) : number
function add (x: string, y: string) : string
function add (x: any,y: any) : string| number {
    return x + y
}

add (1,2)   //3
add('1','2')//12
add(1, '2') //12 원하지 않는데 됨
add('1', 2) //12 원하지 않는데 됨

마지막 두 줄 오류

  • any를 명시적으로 사용하는 처음이자 마지막 사례
    • 사용되는 건 아님
  • 오류 : 두 오버로딩 중 어디에도 해당 X
    • of 뒤의 숫자가 타입 스크립트가 인식하는 오버로딩의 개수

 

선언 순서도 타입 추론에 영향

function example (x: string) : string
function example (x: string| null) : number
function example (x: string| null) : string| number {
    if ( x ) {
        return 'string'
    } else {
        return 123;
    }
}

const result = example('what');	//	const result = string
  • what은 스트링으로 첫번째와 두번째 오버로딩 모두에 해당
    • 여러 오버로딩에 해당되는 경우 제일 먼저 선언된 오버로딩에 해당 -> string

 - 순서 바꾸면

function example (x: string| null) : number
function example (x: string) : string

function example (x: string| null) : string| number {
    if ( x ) {
        return 'string'
    } else {
        return 123;
    }
}

const result = example('what'); //number
  • number로 타입 변화
  • 오버로딩 순서
    • 좁은 타입부터 넓은 타입 순으로

 

인터페이스 오버로딩

interface Add {
    (x: number, y: number) : number;
    (x: string, y: string) : string;
}
const add : Add = (x: any, y: any) => x + y;

add(1,2) //3
add('1','2') //12
add(1,'2') //해당 오버로딩 없음 오류
add('1',2) //해당 오버로딩 없음 오류

 

 

타입 별칭으로 오버로딩 표현

  • 각각의 함수 타입을 선언한 뒤 & 연산자로 하나로 묶음
type Add1 = (x: number, y: number) => number;
type Add2 = (x: string, y: string) => string;
type Add = Add1 & Add2;
const add: Add = (x: any, y:any) => x+y
add(1,2) //3
add('1','2') //12
add(1,'2') //해당 오버로딩 없음 오류
add('1',2) //해당 오버로딩 없음 오류

 

 

 

(주의) 지나친 오버로딩 활용, 유니언이나 옵셔널 매개변수를 활용할 수 있는 경우는 오버로딩 사용 X