Питання на співбесіді з TypeScript
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
extends - успадкування: клас отримує поля й методи батьківського класу (і їхню реалізацію), може їх перевизначити й доповнити.
class Model {
save() { /* загальна логіка збереження */ }
}
class Post extends Model {
title = '';
}
new Post().save(); // метод успадковано
Клас може успадковувати лише один клас.
implements - перевірка відповідності інтерфейсу. Клас обіцяє мати певні члени, але нічого не отримує - реалізацію треба написати самому:
interface Cacheable {
cacheKey(): string;
}
interface Serializable {
toJSON(): object;
}
class Product implements Cacheable, Serializable {
constructor(private id: number) {}
cacheKey() { return `product:${this.id}`; }
toJSON() { return { id: this.id }; }
}
Клас може реалізовувати кілька інтерфейсів.
Важливе непорозуміння: implements не змінює тип класу і не впливає на виведення типів його членів. Він лише перевіряє:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) { // s - неявно any, а не string з інтерфейсу!
return s.length > 0;
}
}
Параметр не отримує тип з інтерфейсу - його треба оголосити явно.
Структурна типізація: клас, який має потрібні методи, і так сумісний з інтерфейсом - навіть без implements. Ключове слово корисне як явний контракт: помилка з'являється в місці оголошення класу, а не там, де його десь передають.
Що обирати:
extends- коли є спільна реалізація, а нащадок справді «є різновидом» батька;implements- коли потрібен спільний контракт без спільного коду;- для повторного використання поведінки часто краща композиція (передати об'єкт-помічник), ніж глибокі ієрархії успадкування.
Інтерфейс може «розширювати» клас (interface X extends SomeClass) - береться лише його форма, разом із приватними членами, що на практиці трапляється рідко.
TypeScript використовує синтаксис ES-модулів: файл з import чи export на верхньому рівні - модуль зі своєю областю видимості.
// money.ts
export function formatPrice(amount: number): string {
return `${amount.toFixed(2)} грн`;
}
export type Currency = 'UAH' | 'USD';
// app.ts
import { formatPrice, type Currency } from './money.js';
Файл без import/export - скрипт: його оголошення потрапляють у глобальну область. Тому в TypeScript-файлах інколи пишуть порожній export {} - щоб файл став модулем.
Чому .js в імпорті .ts-файлу. TypeScript не переписує шляхи імпортів при компіляції: що написано в коді, те й опиниться в зібраному JavaScript. Після компіляції money.ts стане money.js, і Node.js шукатиме саме ./money.js. Тому з moduleResolution: "nodenext" відносні імпорти пишуть з розширенням виконуваного файлу - .js, а TypeScript розуміє, що йдеться про money.ts.
Що залежить від налаштувань:
moduleResolution: "bundler"(Vite, webpack, esbuild) - розширення можна не писати: збирач сам знайде файл. Найзручніше для фронтенду;moduleResolution: "nodenext"- правила Node.js: розширення обов'язкове для ES-модулів, а тип модуля (ESM чи CommonJS) визначається полем"type"уpackage.jsonабо розширенням.mts/.cts;- запуск
.tsнапряму (стирання типів у Node.js, Bun, Deno) - можна імпортувати з.tsзаallowImportingTsExtensions; для бібліотек, що компілюються,rewriteRelativeImportExtensionsперепише.tsна.jsу результаті.
TypeScript 6/7: старі режими moduleResolution: "node" (node10) і "classic" видалено - лишилися nodenext і bundler. Типове значення module - esnext.
Корисне правило: спосіб резолюції модулів у tsconfig має відповідати тому, хто реально виконує код - збирач, Node.js чи інший рушій. Інакше TypeScript погоджуватиметься з імпортами, які не працюють під час виконання.
import type імпортує лише тип - такий імпорт гарантовано зникає з JavaScript після компіляції.
import type { User } from './models.js';
import { fetchUser, type Role } from './api.js'; // змішаний: fetchUser - значення, Role - тип
export type { User };
Навіщо, якщо TypeScript і так прибирає невикористані в коді імпорти типів:
1. Інструменти, що працюють з одним файлом. Babel, esbuild, SWC, стирання типів у Node.js перетворюють кожен файл окремо, не знаючи, що в іншому файлі User - це інтерфейс, а не клас. Для рядка import { User } from './models.js' вони не можуть вирішити, чи лишати імпорт. import type знімає неоднозначність.
2. Помилки виконання. Якщо імпорт типу лишився в JavaScript, а модуль нічого з такою назвою не експортує (інтерфейси під час виконання не існують), ES-модуль впаде: «The requested module does not provide an export named 'User'».
3. Побічні ефекти й цикли. Звичайний імпорт завантажує модуль і виконує його код. import type - ні. Це розриває циклічні залежності, що існують лише на рівні типів, і не тягне важкі модулі заради одного типу.
4. Читабельність - видно, що з модуля потрібні лише типи.
Прапорець verbatimModuleSyntax робить це обов'язковим: імпорт без type, у якому лише типи, - помилка компіляції. Рекомендований для нових проєктів.
Пастки:
import type { Foo }не можна використати як значення -new Foo()чиFoo.staticMethod()дадуть помилку. Для класу, який використовується і як тип, і як значення, - звичайний імпорт;import type X from(за замовчуванням) іimport type * as ns fromтеж працюють;- різниця
import type { A }іimport { type A }: зverbatimModuleSyntaxперший зникає повністю, а другий лишаєimport {} from './mod.js'- модуль усе одно завантажиться заради побічних ефектів.
Лінтер (@typescript-eslint/consistent-type-imports) автоматично виправляє імпорти на import type.
Файл декларацій .d.ts описує лише типи - без реалізації. Він каже TypeScript, які функції, класи й змінні існують і якого вони типу, а сам код живе деінде (у JavaScript-файлі).
// slugify.d.ts
export default function slugify(text: string, options?: { lower?: boolean }): string;
Звідки беруться декларації:
1. Пакет сам постачає типи. Бібліотека, написана на TypeScript, генерує .d.ts при збиранні (declaration: true) і вказує їх у package.json. Більшість сучасних пакетів так і роблять - нічого встановлювати не треба.
2. Пакети @types/* - для бібліотек, написаних на JavaScript без власних типів. Їх пишуть і підтримують спільнотою в репозиторії DefinitelyTyped:
npm install -D @types/lodash
Версія @types/lodash повторює мажорну й мінорну версію самої бібліотеки - їх варто тримати узгодженими.
3. Вбудовані декларації TypeScript - lib.dom.d.ts, lib.es2025.d.ts тощо. Набір обирається опціями target і lib.
4. Власні декларації в проєкті - для бібліотек без типів, глобальних змінних, файлів-ресурсів (*.svg, *.css).
Як TypeScript знаходить типи імпорту: спершу в самому пакеті (types/exports у package.json), потім у node_modules/@types/назва-пакета.
Важлива зміна в TypeScript 6/7: опція types за замовчуванням тепер порожня ([]). Раніше TypeScript автоматично підключав усі пакети з node_modules/@types як глобальні - через це в кожному проєкті були типи process, describe тощо. Тепер глобальні типи треба перелічити явно:
{ "compilerOptions": { "types": ["node", "vitest/globals"] } }
На типи пакетів, які імпортуються (import _ from 'lodash'), це не впливає - лише на глобальні.
skipLibCheck: true - не перевіряти .d.ts залежностей: значно прискорює збирання, а помилки в чужих деклараціях вам однаково не виправити.
Типовий симптом після оновлення: десятки помилок «Cannot find name 'process'», «Cannot find name 'describe'», «Cannot find module 'fs'».
Причина - нове значення опції types за замовчуванням.
- До TypeScript 6: якщо
typesне вказано, компілятор автоматично підключав усі пакети зnode_modules/@typesяк глобальні декларації. Встановлено@types/node- іprocessдоступний будь-де. - TypeScript 6 і 7:
typesза замовчуванням - порожній масив. Жоден пакет@typesне підключається глобально сам по собі.
Рішення - перелічити потрібні глобальні типи явно:
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
Або для різних частин проєкту - різні tsconfig (тести бачать vitest/globals, а код застосунку - ні).
Повернути стару поведінку можна значенням "types": ["*"], але команда TypeScript радить явний список.
Навіщо це змінили:
- швидкість: у сучасних репозиторіях
node_modules/@typesмістить сотні пакетів, підтягнутих транзитивно. Їх розбір і перевірка займали помітну частину збирання - за даними команди TypeScript, явнийtypesприскорював збирання багатьох проєктів на 20-50%; - передбачуваність: глобальні типи тестового фреймворку не «протікають» у код застосунку (у браузерному коді раптом доступний
describeчиprocess).
На що це НЕ впливає: на типи пакетів, які ви імпортуєте. import express from 'express' знайде @types/express як і раніше. Опція types стосується лише глобальних оголошень.
Інші зміни TypeScript 6/7, що дають схожі «раптові» помилки:
rootDirтепер за замовчуванням - каталог зtsconfig.json. Якщо код уsrc/, а результат раптом з'являється вdist/src/, треба вказати"rootDir": "./src";strict: trueза замовчуванням - проєкти, що покладалися на нестрогий режим, мають явно вказати"strict": false(краще - виправити помилки).
Порада: оновлюватися через TypeScript 6 - він показує попередження про застарілі налаштування, а TypeScript 7 робить їх помилками.
Збирачі (Vite, webpack) дозволяють імпортувати не лише код: import logo from './logo.svg'. TypeScript про такі файли нічого не знає і повідомляє «Cannot find module './logo.svg'».
Рішення - ambient-оголошення модуля з шаблоном:
// src/assets.d.ts
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: Readonly<Record<string, string>>;
export default classes;
}
Тепер будь-який імпорт, що закінчується на .svg, має тип рядка (URL файлу).
Vite вже має такі оголошення - досить підключити їх: "types": ["vite/client"] у tsconfig або /// <reference types="vite/client" /> у файлі vite-env.d.ts. Там описано і ?url, ?raw, ?inline, і import.meta.env.
Чому declare module '*.svg' інколи «не працює». Найчастіша причина - файл з оголошенням є модулем: у ньому є import чи export на верхньому рівні.
// assets.d.ts - НЕ спрацює
import type { FC } from 'react';
declare module '*.svg' {
const Component: FC;
export default Component;
}
У файлі-модулі declare module 'назва' - це доповнення (augmentation) існуючого модуля, а доповнити шаблон, якого немає, неможливо. Ambient-оголошення нових модулів мають бути у скрипті - .d.ts без імпортів на верхньому рівні. Якщо тип потрібен з іншого пакета - імпорт всередині блоку:
declare module '*.svg?react' {
import type { FC, SVGProps } from 'react';
const Component: FC<SVGProps<SVGSVGElement>>;
export default Component;
}
Інші причини:
- файл з оголошеннями не входить у
includeчиfilesуtsconfig; - шаблон не збігається (
'*.svg'проти імпорту з?reactна кінці).
Обережно з «заглушками»: declare module 'some-lib'; без тіла робить усі імпорти з пакета типу any - це прибирає помилку, але й будь-яку перевірку. Для бібліотек краще знайти чи написати справжні типи.
tsconfig.json у корені проєкту каже компілятору TypeScript, які файли перевіряти і як це робити. Каталог з цим файлом вважається коренем проєкту.
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"paths": { "@/*": ["./resources/js/*"] }
},
"include": ["resources/js/**/*.ts", "resources/js/**/*.vue"],
"exclude": ["node_modules", "public"]
}
Основні розділи:
include/exclude/files- які файли входять у проєкт.includeприймає шаблони,excludeвідкидає частину з них,files- явний список;compilerOptions- як перевіряти й що генерувати: строгість, цільова версія JavaScript, система модулів, шляхи, глобальні типи;extends- успадкувати базовий конфіг (свій спільний або з пакета на кшталт@tsconfig/strictest,@vue/tsconfig);references- посилання на інші проєкти в монорепозиторії.
Важливо розуміти, хто читає tsconfig.json:
tsc- повністю;- редактор (мовний сервер TypeScript) - для підказок і помилок;
- збирачі (Vite, esbuild) - лише частину опцій, і не перевіряють типи;
- Node.js з вбудованим зняттям типів - не читає взагалі.
noEmit: true - типова опція для застосунків на Vite: JavaScript генерує збирач, а tsc лише перевіряє типи.
Нові значення за замовчуванням (TypeScript 6.0+ і 7.0): strict увімкнено, module - esnext, target - останній стабільний стандарт ECMAScript, types - порожній список. Порожній tsconfig.json сьогодні - вже доволі строга конфігурація.
Практична порада: не копіювати чужий конфіг цілком, а починати з базового пресету фреймворку й змінювати лише те, що розумієте, - багато «магічних» опцій зі старих статей у TypeScript 7 уже видалено.
strict - не одна перевірка, а набір строгих опцій, увімкнених разом:
noImplicitAny- помилка, якщо тип не можна вивести і він став биany(параметри функцій без типів);strictNullChecks-nullіundefined- окремі типи.stringне може бутиnull, і компілятор змушує перевірити значення перед використанням. Найважливіша з усіх;strictFunctionTypes- коректна (контраваріантна) перевірка параметрів функцій;strictBindCallApply- перевірка аргументівbind,call,apply;strictPropertyInitialization- поля класу мають бути ініціалізовані в конструкторі;noImplicitThis- помилка наthisз неявним типомany;useUnknownInCatchVariables- змінна вcatchмає типunknown, а неany;alwaysStrict- режим'use strict'у кожному файлі (у TypeScript 7 його вже не можна вимкнути).
function greet(user: { name: string } | null) {
return user.name; // з strictNullChecks: помилка 'user' is possibly 'null'
}
З TypeScript 6.0 strict увімкнено за замовчуванням. Проєкт, що покладався на старе значення false, має явно вказати "strict": false - і це сигнал, що варто планувати перехід.
Чому не вимикати:
- без
strictNullChecksTypeScript мовчить про найпоширеніші помилки виконання -Cannot read properties of undefined; - без
noImplicitAnyзначна частина коду фактично не типізована; - нові опції строгості з'являються в наборі з новими версіями - проєкт зі
strict: trueотримує їх автоматично.
Міграція великого проєкту, де одразу тисячі помилок:
- вмикати опції по одній (
strict: false+ окремі прапорці), починаючи зnoImplicitAnyіstrictNullChecks; - або вмикати
strictдля нових каталогів через окремі конфіги чи інструменти поступової міграції.
Понад strict корисні noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch - вони в набір strict не входять і вмикаються окремо.
Ці три опції часто плутають, хоча вони відповідають на різні питання.
target - у яку версію JavaScript перетворювати синтаксис.
const user = data?.user ?? guest;
З target: "es2019" компілятор перепише ?. і ?? старим синтаксисом, з es2020 і новіше - лишить як є. target змінює лише синтаксис, а не додає відсутні функції (поліфіли).
У TypeScript 6.0+ значення за замовчуванням - останній стабільний стандарт (зараз es2025), а es5 у TypeScript 7 видалено - найнижча ціль тепер es2015. Для старих браузерів TypeScript-код компілюють іншим інструментом.
lib - які вбудовані API існують у середовищі, для перевірки типів:
"lib": ["es2025", "dom", "dom.iterable"]
Без dom компілятор не знає про document і window; без свіжого es20xx - про Array.prototype.toSorted чи Object.groupBy. Якщо lib не вказано, він береться з target (плюс dom). Для коду в Node.js dom зайвий, натомість потрібен пакет @types/node.
Пастка: lib лише описує типи. Вказати es2025, а запускати код у браузері, який цих методів не має, - помилка виконання, яку TypeScript не впіймає.
module - яку систему модулів генерувати (і як трактувати import/export):
esnext/preserve- лишити ES-модулі (типово для коду, який збирає Vite);nodenext- як Node.js: ES-модулі чи CommonJS залежно від"type"уpackage.jsonі розширень файлів;commonjs-require/module.exports.
amd, umd, systemjs і none у TypeScript 7 вже не підтримуються.
Пов'язана опція moduleResolution - як знаходити файли за шляхом в import: bundler для Vite, nodenext для коду, що виконує Node.js.
Для типового Vite-застосунку: target і module мало впливають на результат (код генерує збирач), але lib визначає, які API бачить редактор, - його варто узгодити з браузерами, які ви підтримуєте.
Замість import Button from '../../../components/Button.vue' зручно писати import Button from '@/components/Button.vue'. Для цього налаштовують псевдоніми шляхів.
У tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./resources/js/*"]
}
}
}
Важлива зміна: раніше разом з paths вказували baseUrl. У TypeScript 6.0 його оголосили застарілим, а в TypeScript 7 - видалено (помилка «Option 'baseUrl' has been removed»). Шляхи в paths тепер пишуть відносно каталогу з tsconfig.json, з явним префіксом ./.
Чому однієї опції paths замало: вона лише каже компілятору, де шукати типи. Код вона не змінює - у зібраному JavaScript лишиться import ... from '@/components/Button.vue', і хтось має перетворити цей шлях на справжній.
Тому той самий псевдонім треба налаштувати в інструменті, що виконує чи збирає код:
- Vite -
resolve.aliasуvite.config.ts(у Laravel-проєктах@часто вже налаштовано плагіном чи шаблоном стартового набору):
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
resolve: {
alias: { '@': fileURLToPath(new URL('./resources/js', import.meta.url)) },
},
});
- Vitest - бере
resolve.aliasз конфігурації Vite; - Node.js з вбудованим запуском TypeScript - не читає
tsconfig.jsonіpathsне підтримує. Альтернатива - subpath imports уpackage.json("imports": { "#/*": "./src/*" }), які розуміють і Node.js, і TypeScript.
Типові помилки:
- псевдонім є в
tsconfig.json, але не у збирачі - редактор задоволений, а збірка падає з «Failed to resolve import»; - різні значення в двох місцях - редактор показує один файл, а в зібраному коді опиняється інший;
pathsу бібліотеці, яку публікують на npm, - у споживачів ці шляхи не працюватимуть; у пакетах кращеimports/exportsзpackage.json.
Глобальні змінні середовища - process, Buffer, describe, it, expect - TypeScript знає не сам, а з пакетів типів: @types/node, @types/jest тощо. Опція types визначає, які з таких пакетів підключати глобально, без явного імпорту.
Що змінилося. До TypeScript 5.9 включно значення за замовчуванням було «усі пакети з node_modules/@types». У великих проєктах це сотні пакетів, підтягнутих транзитивно, - повільна перевірка й випадкові глобальні типи.
З TypeScript 6.0 (і в 7.0) types за замовчуванням - порожній список []. Після оновлення проєкт, що покладався на автоматичне підключення, бачить:
error TS2591: Cannot find name 'process'. Do you need to install type definitions for node?
Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
Рішення - перелічити потрібні пакети явно:
{
"compilerOptions": {
"types": ["node", "vite/client"]
}
}
node- для коду, що працює в Node.js (конфіги, скрипти, SSR);vite/client- типиimport.meta.env,import.meta.hotі імпорту ресурсів (.svg,.css) у Vite-застосунку;vitest/globals- якщо тести використовують глобальніdescribe/it.
Повернути стару поведінку можна значенням ["*"], але документація TypeScript радить явний список: за їхніми даними, багато проєктів пришвидшили збірку на 20-50% лише завдяки цьому.
Що варто знати:
typesстосується лише глобальних оголошень. Пакети, які ви імпортуєте (import express from 'express'), підключаються через імпорт і не потребують запису вtypes;- різні частини проєкту - різні глобальні типи: код браузера не повинен бачити
process, а конфіги Vite -document. Тому шаблони Vite мають два конфіги (tsconfig.app.jsonіtsconfig.node.json) з різнимиtypesіlib; - пакет має бути встановлений: запис у
typesбез@types/nodeуdevDependenciesдасть помилку «Cannot find type definition file».
Типи TypeScript існують лише під час компіляції. Після збирання від них не лишається нічого - браузер виконує звичайний JavaScript. Тому TypeScript перевіряє ваш код, але не дані, що приходять ззовні.
type User = { id: number; name: string; email: string };
const response = await fetch('/api/user');
const user: User = await response.json(); // response.json() повертає any
user.email.toLowerCase(); // компілюється, а якщо API повернув { data: {...} } - падає
Анотація : User - це обіцянка розробника, а не перевірка. Якщо бекенд змінив формат, перейменував поле чи повернув null, TypeScript про це не дізнається - помилка вилізе під час виконання, часто далеко від місця отримання даних.
Звідки беруться «неперевірені» дані:
- відповіді API (
response.json()має типPromise<any>); JSON.parse(тежany);localStorage, параметри URL,postMessage, WebSocket;- змінні оточення, значення полів форм.
Як захиститися - перевірка під час виконання на межі системи:
import * as z from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.email(),
});
type User = z.infer<typeof UserSchema>; // тип виводиться зі схеми
const user = UserSchema.parse(await response.json()); // кине помилку, якщо дані не такі
Схема і тип - одне джерело правди: змінили схему - змінився тип.
Правила:
- дані ззовні мають тип
unknown, доки не перевірені - а неany; - перевіряти один раз на межі (в API-клієнті), а далі код працює з надійно типізованими даними;
- помилка перевірки має бути помітною: зрозуміле повідомлення, лог у моніторинг - так розбіжність між бекендом і фронтендом виявляють одразу.
Альтернативи Zod: Valibot (менший розмір у збірці), ArkType, TypeBox. Ідея однакова - схема під час виконання, з якої виводиться тип.
У JavaScript throw може кинути що завгодно - не лише Error, а й рядок, число, об'єкт чи undefined. Сторонні бібліотеки й старий код справді так роблять. Тому TypeScript не може гарантувати, що в catch прийде Error.
З strict (опція useUnknownInCatchVariables) змінна в catch має тип unknown:
try {
await saveOrder(order);
} catch (error) {
console.log(error.message); // помилка: 'error' is of type 'unknown'
}
Звуження перед використанням:
try {
await saveOrder(order);
} catch (error) {
if (error instanceof ValidationError) {
showFieldErrors(error.errors);
} else if (error instanceof Error) {
showToast(error.message);
} else {
showToast('Невідома помилка');
report(error);
}
}
Допоміжна функція для повідомлення:
function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return 'Невідома помилка';
}
Чому не catch (error: any): це повертає стару небезпечну поведінку - error.response.data.message компілюється й падає з TypeError, якщо помилка мережева й response немає. Явна анотація catch (error: Error) не дозволена - TypeScript не може цього гарантувати.
Пастки з instanceof:
- помилки з іншого вікна (iframe) чи іншої копії бібліотеки в збірці не проходять
instanceof- для них перевіряютьnameчи наявність полів; - власні класи помилок мають правильно наслідувати
Error(class HttpError extends Error) і задаватиname.
Помилки з fetch: fetch не кидає винятку на 404 чи 500 - лише на мережеві помилки. Перевірку response.ok і перетворення на власну помилку (HttpError зі статусом) робить ваш API-клієнт - тоді в catch вона розпізнається через instanceof.
Відхилені проміси - пастка: на параметр колбеку .catch((error) => ...) опція useUnknownInCatchVariables не поширюється, і він має тип any. Його варто явно анотувати: .catch((error: unknown) => ...) - а правило лінтера @typescript-eslint/use-unknown-in-catch-callback-variable нагадає про це. З async/await і try/catch проблеми немає.
Vite передає в клієнтський код змінні оточення з префіксом VITE_ через import.meta.env. За замовчуванням TypeScript знає лише вбудовані поля (MODE, DEV, PROD, BASE_URL, SSR), а власні змінні мають тип any чи не існують.
Підключити типи Vite - у tsconfig.json:
{ "compilerOptions": { "types": ["vite/client"] } }
(з TypeScript 6.0 types за замовчуванням порожній - без цього навіть import.meta.env невідомий).
Описати власні змінні - файл resources/js/env.d.ts (чи src/vite-env.d.ts):
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_NAME: string;
readonly VITE_REVERB_APP_KEY: string;
readonly VITE_REVERB_PORT?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Тепер import.meta.env.VITE_APP_NAME - string, редактор підказує назви, а друкарська помилка VITE_APP_NAM дасть помилку компіляції.
Важливо: тип - не гарантія наявності. Оголошення string не означає, що змінна справді задана в .env на сервері збирання. Відсутня змінна буде undefined в зібраному коді. Надійніше перевірити при старті застосунку:
import * as z from 'zod';
export const env = z
.object({
VITE_APP_NAME: z.string().min(1),
VITE_REVERB_PORT: z.coerce.number().default(443),
})
.parse(import.meta.env);
Неправильна конфігурація виявляється одразу з зрозумілим повідомленням, а не дивною поведінкою в продакшені. Заодно рядкові значення перетворюються на числа й булеві.
Що варто пам'ятати:
- усі значення з
.env- рядки:VITE_FEATURE_X=falseдає рядок'false', який уif- істина; - змінні з префіксом
VITE_потрапляють у зібраний JavaScript і видні будь-кому - секрети туди не кладуть; - значення підставляються під час збирання: зміна
.envна сервері без перезбирання нічого не змінить.
У Node.js-коді (конфіги, SSR) - process.env з типами з @types/node і така сама перевірка схемою.
Докладніше в документації: Vite: IntelliSense для TypeScript
Усі три описують «відповідність ключів значенням», але з різною точністю.
Індексна сигнатура - об'єкт з довільними ключами певного типу:
interface Prices {
[sku: string]: number;
}
Record<K, V> - те саме коротше, але з важливою можливістю: ключі можуть бути обмеженим набором:
type Prices = Record<string, number>; // як індексна сигнатура
type Labels = Record<'draft' | 'published' | 'archived', string>;
const labels: Labels = {
draft: 'Чернетка',
published: 'Опубліковано',
archived: 'В архіві', // пропустити ключ - помилка
};
З обмеженим набором ключів TypeScript вимагає всі ключі - додали новий статус у тип, і компілятор покаже кожен словник, де бракує перекладу. Це дуже корисно для мап статусів, перекладів, конфігурацій.
Map<K, V> - окрема структура даних під час виконання, а не тип об'єкта:
const cache = new Map<number, User>();
cache.set(user.id, user);
const cached = cache.get(5); // User | undefined
Коли що:
Звичайний об'єкт (Record) |
Map |
|
|---|---|---|
| ключі | рядки (і символи) | будь-які: числа, об'єкти |
| порядок | цілочисельні ключі сортуються | порядок додавання |
| JSON | серіалізується | {} - треба перетворювати |
| часте додавання й видалення | повільніше | оптимізовано |
| розмір | Object.keys(o).length |
map.size |
Пастки:
Record<string, V>бреше про наявність ключа:prices['unknown']має типnumber, хоча під час виконання -undefined. РятуєnoUncheckedIndexedAccess(тип стаєnumber | undefined) - уMap.get()це вбудовано;- ключі з даних користувача в звичайному об'єкті можуть зіткнутися з
__proto__чиconstructor. Для довільних ключів -MapабоObject.create(null); - числові ключі в об'єкті стають рядками:
{ 1: 'a' }має ключ'1'.
Правило: фіксований набір ключів - Record з об'єднанням; довільні ключі з даних, що часто змінюються, - Map; дані для JSON (відповіді API, конфіги) - об'єкти.
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії