타입스크립트: 제네릭

공대적 문과생·2022년 1월 23일
0

제네릭

드디어 제네릭까지 왔다. 잘못 해석한 내용이 있으면 호되게 꾸짖어주세요.

제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것이라고 한다. 음 핸드북엔 이렇게 적혀있는데 개인적으로는 잘 와닿지 않는 설명이다. 타입을 미정상태로 놓고, 어떠한 타입이 들어와도 동작하도록 하는 포괄적인(generic) 컴포넌트를 만드는 데에 사용된다.

https://www.typescriptlang.org/docs/handbook/2/generics.html

영문 핸드북에 따르면 이를 위해서 '타입 변수(Type Variable)'라는 것을 이해해야 하는데. 값이 아닌 타입을 담는(works on types rather than values) 변수라고 한다.

	function identity<Type>(arg: Type): Type {
		return arg;
	}

여기서 사용한 Type이 바로 타입 변수이다. 이해한 바에 따르면 꺽쇠를 사용해 변수를 선언한 것처럼 되고, 이후에도 Typing을 해야 하는 자리에 이 타입 변수를 사용한다. 즉 이 타입 변수라는 것은 무언가를 값처럼 담는 변수의 역할이 아니라, 사용될 타입의 종류를 나열한 것에 불과하다.

let output = identity<string>("myString");

타입 변수를 사용해 선언한 함수를 호출할 때, 인수 앞에 역시 꺽쇠를 사용해 타입을 명시해준다. 여기서는 identity() 함수의 인수, 반환이 모두 string으로 적용된다.

let output = identity("myString");

또는 위와 같이 타입 인수 추론(type arguments inference)에 기대하는 것도 방법이다. 컴파일러가 추론에 실패할 경우에는 전자처럼 타입을 밝혀준다.

다음과 같은 코드는 에러를 내뿜는다.

function loggingIdentity<Type>(arg: Type): Type {
	console.log(arg.length); // Property 'length' does not exist on type 'Type'.
	return arg;
}

선언된 타입 변수 Type에는 모든 타입이 올 수 있다. 그러나 그 모든 타입은 .length 멤버를 가지고 있을 거라는 보장이 없다. 따라서 아래와 같이 써야 한다.

function loggingIdentity<Type>(arg: Type[]): Type[] {
	console.log(arg.length);
	return arg;

여기서 개인적으로 오해가 생겼었는데, 앞에서 <Type>으로 선언된 변수는 Type 타입이고, 뒤에 파라미터로 언급된 Type[]Array<Type> 타입이기 때문에 논리적으로 이상하지 않은가 하는 것이었다. 예를 들면 string이 전달된다고 할 때 Typechar 타입일 것이고 Type[]string이 될 것이기 때문에 서로 상충하지 않은가? 라고 생각했지만

앞의 <Type>은 단순히 이 Type을 사용하겠다고 선언하는 것과 같기 때문에 실질적으로 하는 역할이 없다. 뒤의 파라미터와 리턴을 설정하는 부분에 이 Type의 배열을 사용하겠다는 부분이 실질적으로 중요하다. 즉 파라미터와 리턴이 '어떤 타입의 배열'이라는 것을 명시하기 위해 기준이 되는 Type을 선언한 것 뿐. 따라서 이런 구문은 함수가 받을 수 있는 타입을 '어떤 타입의 배열' 즉 char의 배열인 string이나, number등의 배열로 '한정하는' 결과를 가져온다.

참고: 자바스크립트에는 char 타입이 없다고 한다.

또는 아래와 같은 방법이 있다.

interface LengthWise {
	length: number;
}

function logText<T extends LengthWise>(text: T): T {
	console.log(text.length);
	return text;
}

이렇게 하면 배열이 아니더라도, length 속성을 가진 객체를 받을 수 있다.

갑자기 알아보는 선언(declaration)과 정의(definition)의 차이

https://banaba.tistory.com/41
댓글의 지적도 참고.

https://zetawiki.com/wiki/C%EC%96%B8%EC%96%B4_%EC%84%A0%EC%96%B8%EA%B3%BC_%EC%A0%95%EC%9D%98_%EC%B0%A8%EC%9D%B4%EC%A0%90

결정적인 차이는 메모리에 할당하느냐 아니냐의 차이이다.

  1. 선언: 컴파일러가 참조할 식별자와 이름을 알림

    • 선언은 메모리 영역에 올리지 않기 때문에, 중복되어도 상관이 없다. 여러번 해도 됨.
  2. 정의: 식별자와 이름으로부터 코드를 생성하여, 함수가 호출되거나 변수를 사용할 때 생성된 코드를 참조함.?

    • 정의는 중복될 수 없다. 컴파일 에러 발생.

call signature 와 function type expression

https://stackoverflow.com/questions/32043487/difference-between-call-signature-and-function-type

call signature:

(arg: T):T

function type expression:

(arg: T) => T

잠깐만 훑어보았다. 지금 수준에서 아주 자세히 알아야 할 내용은 아닌 것 같음 ..

제네릭 클래스

클래스에는 static 사이드와 instance 사이드가 있는데, 제네릭은 여기서 instance 사이드에서만 사용할 수 있다고 한다.

static 이라는 것은 해당하는 구역 안에서, 그리고 그 하위의 블록에서만 사용되고 외부에서 액세스할 수 없는 것들을 뜻한다.

객체의 속성을 비교

두 객체를 비교할 때도 제네릭 제약 조건을 사용할 수 있다.

function getProperty<T, O extends keyof T>(obj: T, key:O) {
	return obj[key];
}

let obj = {a:1, b:2, c:3};

getProperty(obj, "a");
getProperty(obj, "z"); // error. "z"는 "a" | "b" | "b" 에 속하지 않음.

keyof

keyof 키워드는 해당 타입에 존재하는 프로퍼티의 각 키 string을 , 유니언 타입으로 돌려준다.

interface Todo {
	id: number;
	text: string;
	due: Date;
}

type todoKeys = keyof Todo; 
// "id" | "text" | "due"

제네릭 관련해서는 너무 어려우니까 여기까지만 공부하고 멈춰야겠다. 헉헉

profile
공대적 문과생, 추남적 미남, 여성적 남성, 신사적 변태, 이론적 로맨티시스트, 현실적 이상주의자.

0개의 댓글