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!