Type inference(타입 추론)
let a = "hello" // implicit
let b : boolean = false // explicit
//if you do like this, compiler tells it has error
b = "false";
//another example
let c = [1,2,3]
c.push("1") // error
let c : number[] = [] // when you want to create empty array
c.push(1) // no error
TypeScript
복사
Types
규칙
let a = [1,2];
let b : string[] = ["i1","1"];
let c : boolean[] = [true];
TypeScript
복사
optional
// 어떤 애들음 name이 있고 어떤 애들은 age가 있을 때
const player : {
name: string,
age?: number
} = {
name: "nico"
}
//이렇게 생각할 수 있다.
const player : object = {
name: "nico"
}
player.name; // error
/*
왜 그러냐
: 뒤에는 타입이 들어간다. 정확하게 얘기하면 어떤 property를 가지는 object인지가 명시되는 듯
따라서 그냥 object라고 써버리면, object는 아무 프로퍼티도 가지지 않으므로 에러가 뜨는 듯하다.
따라서 어떤 타입을 만든다면 맨 처음 처럼 해줘야함.
*/
//드는 의문, 클래스 어떻게 정의하냐?
//자바랑 비슷하네, 클래스를 그냥 정의하면 되는구나
TypeScript
복사
type alias
type Player = {
name: string,
age?: number
}
const nico : Player = {
name: "nico"
}
const lynn : Player = {
name:"nico",
age: 12
}
TypeScript
복사
→ type alias 대신에 클래스를 쓰면 되는거 아닌가?
보니까 세 가지가 있네. interface, type alias, class
OOP의 원칙에 좀 더 맞게 사용하려면 사실 interface는 그냥 타입이고 class는 생성되는 객체이다.
아직 잘 모르겠지만 class에 컨스트럭터가 필수적으로 있어야하는 것처럼 보인다.
따라서 대부분의 경우 일반 자바처럼 하면 될 듯.
다만 조금 다른 점을 꼽자면, duck typing이라고 하는게 있네
애초에 타입 검사를 할 때 어떻게 하냐에서 출발한 문제인거 같은데, typeScript는 같은 시그니처의 메서드나 프로퍼티가 있다면 구현한 것으로 보는 듯.
뿐만 아니라 아래처럼 프로퍼티가 요구하는 사항보다 넘쳐도 마찬가지인듯
interface IPerson {
name: string;
}
function sayHello(person: IPerson): void {
console.log(`Hello ${person.name}`);
}
const me = { name: 'Lee', age: 18 };
sayHello(me); // Hello Lee
TypeScript
복사
function rerturn type
type Player = {
name: string,
age?: number
}
const nico : Player = {
name: "nico"
}
const lynn : Player = {
name:"nico",
age: 12
}
function playerMaker(name:string) : Player{
return {
name:name
}
}
const playerMaker = (name:string) : Player => ({name}); // arrow function
const larry = playerMaker("larry");
larry.age
TypeScript
복사
readonly property to type
type Player = {
readonly name: string,
age?: number
}
const lynn : Player = {
name:"nico",
age: 12
}
const playerMaker = (name:string) : Player => ({name});
const nico = playerMaker("nico");
nico.age = "las";
//another example
const numbers : readonly number[] = [1,2,3,4,5];
numbers.push(6);
TypeScript
복사
tuple
// 정해진 순서에 맞는 배열이 탄생~
const player: [string,number,boolean] = ["nico",1,true];
player[0] = "larry";
const player: readonly [string,number,boolean] = ["nico",1,true];
TypeScript
복사
javascript에서 const는 재선언과 재할당이 안 되는거지, 프로퍼티의 값은 변경할 수 있는 거다.
let은 재할당도 가능함.
any
any
TypeScript
복사
타입 스크립트의 보호장치에서 벗어나고 싶으면 쓰는거다. 그냥 쓰지 마셈.
참고로 null과 undefined의 차이는 null은 값이 할당되었지만, 빈 값일 때이다. 즉 빈 오브젝트이고
undefined는 primitivie 타입으로서, 아직 값이 할당이 되지 않은 상태를 얘기함.
unknown & void & never
let a:unknown; //api로부터 응답이 왔는데, 어떤 타입일지 모를때
if(typeof a === 'number'){
let b = a+1;
}
if(typeof a === 'string'){
let b = a.toUpperCase();
}
//return nothing
function hello(){
console.log('x');
}
// 함수가 절대 return하지 않을 때, never 타입
function foo2(x: string | number): boolean {
if (typeof x === "string") {
return true;
} else if (typeof x === "number") {
return false;
}
return fail("Unexhaustive!");
}
function fail(message: string): never { throw new Error(message); }
let tmp: any = {id:"123"}
console.log(foo2(tmp))
TypeScript
복사
Functions
function type Expressions
function greeter(fn:{a:string} => void){
fn("hello world");
}
function prinToConsole(s: string){
console.log(s);
}
greeter(printToConsole);
type GreetFunctions = (a:string) => void;
function greeter(fn:GreetFunctions) {
}
TypeScript
복사
위와 같은 방식으로 function에 대한 type expression을 할 수 있다.
타입 스크립트 독스에 보면, 타입 스크립트에서 type은 그저 set of Data라고 생각하면 편하단다. 따라서 함수 또한 일급 객체인 자바스크립트에서, 각 함수별로 타입을 표현할 수 있을텐데, 위의 예시처럼 표현해준다.
call signature
type DescribaleFunction = {
descripton: string;
(someArg: number): boolean;
};
function doSomething(fn:DescribableFunction){
console.log(fn.description + " returned " + fn(6));
}
TypeScript
복사
함수 타입을 정의할 수 있다. 이것을 call signature라고 한다.s
자바스크립트에서 함수는 callable한 특성 외에 properties를 가질 수 있다.
하지만 function type expression은 이것을 허용하지 않기에, object type으로 call signature를 써준다.
Construct Sginatures
type SomeConstructor = {
new (s:string): SomeObject;
}
function fn(ctor: SomeConstructor){
return new ctor("hello");
}
TypeScript
복사
객체를 생성하는 특수한 함수인 constructor가 있다.
이 constructor 또한 함수이기에 signature를 생성해줄 수 있고, 이것을 new를 통해 호출할 수 있다.
Generic Functions
// type SuperPrint = {
// <TypePlaceHolder>(arr:TypePlaceHolder[]):void,
// }
type SuperPrint = <T>(a: T[]) => T;
const superPrint:SuperPrint = (arr) => {
arr.forEach(i=>console.log(i));
return arr[0];
}
superPrint([1,2,3,4]);
superPrint([true,false,true]);
superPrint(["a","b","c"]);
type GamePlayer<E> = {
name:string
extraInfo:E
}
type NicoExtra = {
favFood:string
}
type NicoPlayer = GamePlayer<NicoExtra>
const nnico:NicoPlayer = {
name:"nico",
extraInfo:{
favFood:"kimchi"
}
}
const llynn: GamePlayer<null> = {
name:"lynn",
extraInfo:null
}
TypeScript
복사
기본적으로 사용할 때는 typeScript가 자동으로 타입을 inference한다.
뿐만 아니라 자바처럼 Generic에 대한 constraint를 줄 수 있는 듯 하다.
function longest<Type Extends {length:number}>(a: Type, b: Type){
if(a.length >= b.length){
return a;
}else{
return b;
}
}
//longerArray is of type 'number[]'
const longerArray = longest([1,2], [1,2,3]);
//longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
//Error! Numbers don't have a 'length' property
const notOK = longest(10,100);
TypeScript
복사
generic에 대해서 explicit하게 inference하도록 알려줄 수 있다.
function combine<Type>(arr1: Type[]. arr2: Type[]): Type[]{
return arr1.concat(arr2);
}
const arr = combine([1,2,3],["hello"]); // error
// string is not assignable to type 'number'
//근데 이건 또 안됨
function combine<Type1,Type2>(arr1:Type1[], arr2: Type2[]){
return arr1.concat(arr2);
}
//array에 대해서 concat할 수 있는 거는 같은 타입이여야 한다.
//해결 방법
const arr = combine<string | number>([1,2,3],["hello"]);
//이거는 type이 string | number인 union 타입이라서 가능한건데, 대체 union type이 뭐냐?
/*
type은 그냥 데이터의 set이기 때문에, 그 set을 union 시켜놓은 타입이다.
union type은 따라서, union의 모든 멤버에 대해서 valid한 연산만 허용한다.
따라서 이 set들을 함수 내부에서 분리시켜주는 작업이 필요한데, 이게 바로 narrow라고 하는 거다.
narrow는 기본적으로 set을 분리시키는 걸 의미하기에,
어떤 방식으로든 잘 분리하면 그만이다.
1. if-else
2. equality
3. truthiness
4. in operator
5. instanceof
쓰는 방법 정도가 있겠네.
*/
TypeScript
복사
Function Overloads
//방법1: 다양한 function expression을 type으로 일단 정의해놓고, 구현하고 구현할 때 narrowing 한다.
type Add = {
(a:number, b:number) : number,
(a:number, b:string) : number,
(a:number, b:number, c:number): number
}
const add:Add = (a,b,c?) => {
if(typeof b === "string") return a;
else{
return a+b;
}
}
//방법2: overloading을 정의하는 파트와 구현부를 분리해서 생각한다.
//Overload signatures
function greet(person: string): string;
function greet(person: string[]): string[];
//implementation
function greet(person: unknown) : unknown {
if(typeof person == 'string'){
return `Hello. ${person}!`;
}else if(Array.isArray(person)){
return person.map(name => `Hello, ${name}!`);
}
throw new Error('Unable to greet');
}
//여기서 중요한 것은 마지막 구현부는 overload되는 모든 함수 signature를 포함할 수 있어야한다는 것!!
TypeScript
복사
말 그대로 다양한 argument counts나 type에 대해서 호출하고 싶을 때 사용하는 거다.
위에 처럼 두 가지 방법이 있는 듯하다.
Function overload를 할 때는 몇 가지 가이드라인이 있는 듯 하다.
1.
specific overload타입을 무조건 먼저 써라.
a.
타입 체킹을 위해서부터 하는 듯. 따라서 허접한게 먼저 나오면 무조건 거기서 다 매치된다.
2.
Optional Parameter를 써라
a.
따라오는 파라미터가 있는 경우에는 오버로딩을 하지 마라. 대신 optional이 가능하다면 최대한 그렇게 하기를...
b.
왜냐?
i.
일단 signature compatibility, 즉 signature가 넘겨도 되는건지 검사를 할 때 어떤 방식으로 하냐
1.
source의 argument들을 넘길 때, 불러내는 메소드가 invoke 가능하면 합당하다고 판단한다.
interface Example{
diff(one: string): number;
diff(one: string, two: string): number;
diff(one: string, two: string, three: boolean): number;
}
interface Example {
diff(one:string, two?: string, three?: boolean): number;
}
function fn(x: (a: string, b: number, c: number) => number){
let result: number = x("1", 2,3);
console.log(result);
}
let x: Example = {
diff(one:string, two?:string, three?:boolean): number{
if(three){
console.log(three);
console.log(typeof three);
return 3;
}else if(two){
console.log(two);
return 2;
}else{
console.log(one);
return 1;
}
}
}
}
fn(x.diff)
TypeScript
복사
이 예시에서 x.diff에 해당하는 함수를 fn이 요구하는 타입
즉, (a: string, b: number, c: number) ⇒ number에 맞출 수 있을 것이냐가 관건인데, 원래는 가능했던 이유가 fn 내부에서 x를 호출했을 때, 어떤 타입을 오버로딩할 수 있냐를 봤을 때, 첫 번째 형식의 함수, diff(one: string): number에 해당하는 형식에 매칭이 된다. 즉 첫 번째 parameter의 타입이 일치하고, 해당 오버로딩을 invoke할 수 있기 때문에, 합당하다고 판단하고, 뒤에 것들은 extraneous argument로 판단하고 그냥 넘겨버리는 것.
optional parameter를 사용했을 때만 방어가 가능한 버그다.
ii.
하나의 파라미터만 있다면, union 타입을 써라
interface Moment{
utcOffset(): number;
utcOffset(b: number): Moment;
utcOffset(b: string): Moment;
}
interface Moment{
utcOffset(): number;
utcOffset(b: number | string): Moment;
}
TypeScript
복사
Object Types
index Signature
interface StringArray{
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
TypeScript
복사
이 때 index signature의 프로퍼티는 number이거나 string이여야한다.
index signature는 ‘dictionary’를 짜게하는 좋은 방법이지만, 모든 프로퍼티의 return type을 통일시킬 것을 강요한다. 왜냐하면 obj.property로 접근할 수도 있지만은 obj[”property”]로도 접근할 수 있어야하기 때문이다. 따라서 리턴 타입이 통일되어야함.
다만 union type으로 해놓은 경우에는 달라도 됨.
Extending Types
interface BasicAddress{
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress{
unit: string;
}
TypeScript
복사
extends를 쓰면 원래 타입을 그대로 복사해온다.
Intersection Types
interface Colorful{
color: string;
}
interface Circle{
radius: number;
}
type ColorfulCircle = Colorful & Circle;
TypeScript
복사
Colorful과 Circle에 있는 모든 멤버들을 가진 새로운 타입을 생성한다.
Type Manipulation
keyof
object type을 받아서 string이나 numberic literal union of its keys를 반환한다.
type Point = { x: number; y: number{;
type P = keyof Point;
type Arrayish = {[n:number]: unknown};
type A = keyof Arrayish; // type A = number
type Mapish = {[key:string]: boolean};
type M = keyof Mapish; // type B = string | number
TypeScript
복사
Classes & Interfaces
class Point{
x: number;
y: number;
constructor(x = 0, y = 0){
this.x = x;
this.y = y;
}
}
class Point{
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any){
//구현
}
}
TypeScript
복사
Implements
interface Checkable{
check(name: string): boolean;
}
class NameChecker implements Checkable{
check(s){
return s.toLowercse() === "ok";
}
}
TypeScript
복사
implements는 해당 클래스가 해당 interface로 취급될 수 있는지를 나타내는 거지. 바꾸는게 아니다.
따라서 s의 타입은 any이다. 명시해주지 않았기 때문에.
비슷하게 implementing interface는 optional property를 생성해주는 것은 아니다.
interface A{
x: number;
y?: number;
}
class C implements A{
x = 0;
}
const c = new C();
c.y = 10;
TypeScript
복사
Overriding
class Base{
greet(){
console.log("Hello, world!");
}
}
class Derived extends Base{
greet(name?: string){
if(name === undefined){
super.greet();
}else{
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
TypeScript
복사
오버라이딩을 할꺼면 base 클래스에 해당하는 규약을 좀 맞출 필요가 있다.
그니까 이런 경우는 안됨.
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// Make this parameter required
greet(name: string) {
/*
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
Type '(name: string) => void' is not assignable to type '() => void'.
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
Type '(name: string) => void' is not assignable to type '() => void'.
*/
console.log(`Hello, ${name.toUpperCase()}`);
}
}
TypeScript
복사
다른건 일반적인 클래스랑 비슷한듯?