Poznajemy bądź przypominamy sobie kilka słówek kluczowych TypeScripta. Extends i infer zostawimy sobie na następną lekcję, powiem tylko, że będzie mega. Zaczynajmy.

Ok, najpierw słówko typeof. Zróbmy sobie takiego Johna:

let someJohn = {
    name: "John",
    lastName: "Doe",
    age: 30,
    sayHello() {
        console.log(`Hi my name is ${this.name} ${this.lastName}`);
    }
};

Taki właśnie someJohn to obiekt. Typeof używamy na obiekcie (albo innej zmiennej) aby wyłuskać definicję typu z niego od tej strony:

let someJohn = {
    name: "John",
    lastName: "Doe",
    age: 30,
    sayHello() {
        console.log(`Hi my name is ${this.name} ${this.lastName}`);
    }
};

type JohnType = typeof someJohn;

// type JohnType = {
//     name: string;
//     lastName: string;
//     age: number;
//     sayHello(): void;
// }

Typeof możemy też używać do zdobywania definicji typu funkcji:

let someJohn = {
    name: "John",
    lastName: "Doe",
    age: 30,
    sayHello() {
        console.log(`Hi my name is ${this.name} ${this.lastName}`);
    }
};

type JohnMethod = typeof someJohn["sayHello"];

//type JohnMethod = () => void

Już to robiliśmy, z takimi utility types jak ReturnType czy Parameters:

function getObj(){
    return {name: "John", age: 30 }
};

type getObjFunc = typeof getObj;

// type getObjFunc = () => {
//     name: string;
//     age: number;
// }

type objType = ReturnType<typeof getObj>

// type objType = {
//     name: string;
//     age: number;
// }

Teraz przykład z parameters:

function handleRequest(url: string, method: 'GET' |'POST'){

  }

type handleReqFunc = typeof handleRequest;

//type handleReqFunc = (url: string, method: 'GET' | 'POST') => void

type handleRequestParams = Parameters<typeof handleRequest>;

//type handleRequestParams = [url: string, method: "GET" | "POST"]

Widzimy co robi typeof na funkcjach oraz co robi z tymi utility types. Ok, wracamy do Johna. Operator keyof działa tylko na typach:

let someJohn = {
    name: "John",
    lastName: "Doe",
    age: 30,
    sayHello() {
        console.log(`Hi my name is ${this.name} ${this.lastName}`);
    }
};

type JohnKeys = keyof typeof someJohn;

//type JohnKeys = "name" | "age" | "sayHello" | "lastName"

Tutaj od strony obiektu wyłuskujemy definicję typu, następnie wyciągnięcie kluczy.

Ok, poznajmy sobie kolejny utility type, czyli record:

type Streams = 'salary' | 'bonus' | 'sidehustle'

type Incomes = Record<Streams, number>

const monthlyIncomes: Incomes = {
    salary: 500,
    bonus: 100,
    sidehustle: 250
}

To jest właśnie record. Coś, co ma klucze takie jak w pierwszym argumencie i wartość jak w drugim argumencie. Record to mapped type i mapped types też sobie poznamy, ale jeszcze nie teraz.

I teraz pętla z keyof:

for (const revenue in monthlyIncomes) {
    console.log(monthlyIncomes[revenue as keyof Incomes])
}

Nad as jeszcze się pochylimy, na razie jednak pamiętajmy – keyof na typie, wyłuskuje klucze typu. Tutaj mówimy, że revenue musi być kluczem typu Incomes. A ten typ to record, jego kluczami jest Streams, typ wartości tych kluczy to number.

Przypomnijmy sobie tę funkcję generyczną:

const getObjKeyVal = <T extends object, K extends keyof T> (obj: T, key: K) => 
    {
        return obj[key];
    }

Teraz zastosujmy ją na takim obiekcie:

interface Student {
    name: string,
    GPA: number,
    classes?: number[]
}

const student : Student = {
    name: "Jane",
    GPA: 3.5,
    classes: [100, 200]
};

console.log(getObjKeyVal(student, 'name')); //Jane

A teraz ścisła funkcja, tylko Student, ale też warunek dla klucza:

const getStudentKeyVal = (student: Student, key: keyof Student) => 
    {
        return student[key];
    }

console.log(getStudentKeyVal(student, 'name')); //Jane

Zobaczmy te loopy:

for (const key in student) {
    console.log(`${key}: ${student[key as keyof Student]}`)
};

Object.keys(student).map(key => {
    console.log(student[key as keyof Student]);
});

Tutaj bierzemy keyof po typie, natomiast lepiej by było go wyłuskać z obiektu, w końcu możemy nie wiedzieć, jaki ma typ:

for (const key in student) {
    console.log(`${key}: ${student[key as keyof typeof student]}`)
};

Object.keys(student).map(key => {
    console.log(student[key as keyof typeof student]);
});

Ok, przykład z internetu na użycie as:

interface student {
    n: string
    id: number
}

function getstudent(){
    let name : string = "yash";
    return {
        n: name, 
        id: 89
    };
}

let student  = getstudent() as student;

Tu chodzi o to, że nigdzie nie określiliśmy, że getstudent zwróci taki typ. Mało tego, są funkcje (np. querySelector), które mogą zwrócić undefined. Także funkcje fetch często nie wiadomo co zwracają.

I as tutaj nam pozwala określić, że mamy tę wartość traktować jako taki a taki typ.

As const nie będziemy przypominać, za to taki przykład podam:

class Collection<T>  {

    constructor(private _data: T[]) {
      this._data = _data;
    }
  
    get length(): number {
      return this._data.length;
    }

    get(key: number): T | -1 {
        if(key >= this.length)
            return -1;
        return this._data[key];
    }

    get data() : T[]{
        return this._data;
    }

    set data(newData: T[]) {
        this._data = newData;
    }

    push(val: T) : void {
        this._data.push(val);
    }
  
    
  }

let col1 = new Collection([1,2,3]);

console.log(col1.length);
console.log(col1.get(0));
console.log(col1.get(10)); //-1

col1.push(4);

Jest ok, nie? A teraz przekażmy pustą tablicę:

let col1 = new Collection([]);

console.log(col1.length);
console.log(col1.get(0));
console.log(col1.get(10)); 

col1.push(4); //error

console.log(col1.data); 
col1.data = [4,3,2,1]; //error
console.log(col1.data); 

Można to obejść podając typ:

let col1 = new Collection<number>([]);

console.log(col1.length);
console.log(col1.get(0));
console.log(col1.get(10)); 

col1.push(4); //ok

console.log(col1.data); 
col1.data = [4,3,2,1]; //ok
console.log(col1.data); 

Ale można też użyć operatora as:

let col1 = new Collection([] as number[]);

console.log(col1.length);
console.log(col1.get(0));
console.log(col1.get(10)); 

col1.push(4); //też ok

console.log(col1.data); 
col1.data = [4,3,2,1]; //też ok
console.log(col1.data);

Ok, z as const jest tak:

const nums1 = [1,2,3];
//const nums1: number[]

const nums2 = [1,2,3] as const;
//const nums2: readonly [1, 2, 3]

Jak widać, po pierwsze literalnie są rozumowane typy (1|2|3 zamiast number), po drugie readonly.

Pokazywałem już, do czego to się przydaje:

function handleRequest(url: string, method: 'GET' |'POST'){

  }

let req1 = {url: 'www.google.com', method: 'GET'};

handleRequest(req1.url, req1.method); //źle

const req2 = ["www.google.com", 'GET'];

handleRequest(...req2); //źle

As const rozwiązuje problem:

let req1 = {url: 'www.google.com', method: 'GET'} as const;

handleRequest(req1.url, req1.method); //też ok

const req2 = ["www.google.com", 'GET'] as const;

handleRequest(...req2); //ok

Choć są też inne podejścia, pamiętajmy o utility types, które w takim i wielu innych przypadkach pomogą nam upewnić się, że przekazujemy do funkcji odpowiednie parametry:

const req2 = ["www.google.com", 'GET'] as Parameters<typeof handleRequest>;

handleRequest(...req2); //też ok

Warto na jeszcze jedną rzecz zwrócić uwagę, jeżeli chodzi o readonly arrays:

const nums1 = [1,2,3];
//const nums1: number[]

const nums2 = [1,2,3] as const;
//const nums2: readonly [1, 2, 3]

type ArrLen<A extends readonly unknown[] > = A["length"];

type Nums2Len = ArrLen<typeof nums2>;
//type Nums2Len = 3

type Nums1Len = ArrLen<typeof nums1>;
//type Nums1Len = number

Jak widać mają fixed length. Ok, więcej TSa niedługo!