TypeScript Basics #7: Utility Types and tsconfig
Last time we covered the deeper tools of generics. This is the final post in the series. We wrap up with two topics — the standard utility types you’ll use every day in real code, and the key options of tsconfig.json that control compile behavior.
What are utility types? #
TypeScript ships built-in utility types for the type-transformation patterns used most often. You can use them anywhere without an import.
type User = { id: string; name: string; age: number; email: string };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }
type UserUpdate = Partial<User>;
// = { id?: string; name?: string; age?: number; email?: string }
These transformations are built with tools like the mapped types and indexed access we saw in #6 — they’re expressible in TypeScript itself. You could write them yourself, but the common ones are in the standard library, so use those.
Let’s look at the headline ones by category.
Object transforms — Partial / Required / Readonly #
Partial<T> — make every property optional
#
type User = { id: string; name: string; age: number };
type UserPatch = Partial<User>;
// = { id?: string; name?: string; age?: number }
It’s frequently used as the input type of an update function.
function updateUser(id: string, patch: Partial<User>): void {
// find the user by id and overwrite only the fields in patch
}
updateUser('u-1', { name: '영희' }); // ✓
updateUser('u-1', { name: '영희', age: 28 }); // ✓
Required<T> — make every optional required
#
The opposite of Partial. Properties marked with ? become required.
type Config = { host?: string; port?: number; timeout?: number };
type CompleteConfig = Required<Config>;
// = { host: string; port: number; timeout: number }
Readonly<T> — make every property readonly
#
type ImmutableUser = Readonly<User>;
const u: ImmutableUser = { id: 'u-1', name: '철수', age: 30 };
u.name = '영희'; // 🚫
Key selection — Pick / Omit / Record #
Pick<T, K> — keep only some keys
#
type User = { id: string; name: string; age: number; email: string };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }
K is a union of T’s keys. The new type is built from only the picked properties.
Omit<T, K> — drop some keys
#
type UserWithoutEmail = Omit<User, 'email'>;
// = { id: string; name: string; age: number }
type UserWithoutEmailAndAge = Omit<User, 'email' | 'age'>;
// = { id: string; name: string }
To drop multiple keys, pass them as a union. Pick and Omit are near mirror images. “Pick what you need” is Pick; “drop what you don’t” is Omit.
Record<K, V> — key-value mapping object
#
type Scores = Record<string, number>;
// = { [key: string]: number }
const scores: Scores = {
math: 95,
english: 87,
science: 91,
};You can also restrict it to a specific union of keys.
type Roles = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Roles, string[]>;
const perms: Permissions = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read'],
};
// missing any of the three is a compile error
This pattern is powerful for safely expressing data mapped to enum-like values.
Union transforms — Exclude / Extract / NonNullable #
Exclude<T, U> — drop some types from a union
#
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Exclude<AllColors, 'green' | 'blue'>;
// = 'red' | 'yellow'
Drops the types in U from the union T.
Extract<T, U> — keep only some types from a union
#
type Mixed = string | number | boolean | null | undefined;
type Truthy = Extract<Mixed, string | number | boolean>;
// = string | number | boolean
Mirror of Exclude.
NonNullable<T> — drop null/undefined
#
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// = User
A utility you’ll use often once you start dealing with T | null | undefined regularly.
Function-related — ReturnType / Parameters / Awaited #
ReturnType<F> — a function’s return type
#
function fetchUser() {
return { id: 'u-1', name: '철수' };
}
type User = ReturnType<typeof fetchUser>;
// = { id: string; name: string }
This is a pattern we briefly saw in #6 — you pull the shape of an object a function returns automatically, without defining a separate type.
Parameters<F> — a function’s parameter types (as a tuple)
#
function login(username: string, password: string): boolean {
return true;
}
type LoginArgs = Parameters<typeof login>;
// = [username: string, password: string]
Awaited<T> — unwrap a Promise
#
type UserPromise = Promise<User>;
type UnwrappedUser = Awaited<UserPromise>;
// = User
// nested Promises also unwrapped
type Nested = Promise<Promise<User>>;
type Unwrapped = Awaited<Nested>;
// = User (unwrapped through nesting)
Frequently used for handling the return type of async functions.
Combined example — a realistic pattern #
A small example combining the utilities we’ve seen.
type User = {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
};
// Exposed to clients — drop password
type PublicUser = Omit<User, 'password'>;
// Data from a signup form — id and createdAt are decided by the server
type SignupInput = Omit<User, 'id' | 'createdAt'>;
// Profile update — all optional except id
type ProfileUpdate = Partial<Omit<User, 'id'>>;
// User list card — only some fields needed
type UserCard = Pick<User, 'id' | 'name'>;
// Permission mapping
type Role = 'admin' | 'editor' | 'viewer';
type RolePermissions = Record<Role, string[]>;
// Pull parameters out of an API function signature
async function getUser(id: string): Promise<PublicUser> {
// ...
return {} as PublicUser;
}
type GetUserArgs = Parameters<typeof getUser>; // [id: string]
type GetUserResult = Awaited<ReturnType<typeof getUser>>; // PublicUser
From the same User data shape, you derive every variant you need per situation. Define the data shape in one place and derive views from it. The data-type sync burden disappears, and changes to User automatically propagate to derived types.
tsconfig.json — compiler settings #
Now to the second topic. tsconfig.json is the configuration file that controls every behavior of the TypeScript compiler.
The default config created by npx tsc --init comes with detailed comments. Open it once and you’ll see over 100 options. Fortunately, only a dozen or so matter day to day.
The key option — strict #
{
"compilerOptions": {
"strict": true
}
}strict: true is the master switch that turns on several safety options at once.
noImplicitAny— bans implicit any when inference failsstrictNullChecks— clearly distinguishesnullandundefinedfrom other typesstrictFunctionTypes— strict function-parameter type compatibilitystrictBindCallApply— checks the arguments tobind/call/applystrictPropertyInitialization— enforces class property initializationnoImplicitThis— bans implicit-anythis
For new projects, always start with strict: true. Turning it off cuts more than half the value of TypeScript. For migrating existing JavaScript projects, you may turn it on gradually, but turning strict off in a new project is almost always a wrong choice.
target — which JavaScript to compile to #
{ "compilerOptions": { "target": "ES2022" } }Decides which version of JavaScript TypeScript compiles to.
ES5— when you must support old IE (rare now)ES2017–ES2022— modern browsersESNext— latest
For browser targets, usually ES2020 or above; for the latest Node.js, around ES2022 is reasonable.
module — module system #
{ "compilerOptions": { "module": "ESNext" } }Decides how import/export code is compiled.
CommonJS— Node.js’s traditionalrequireESNext/ES2022— standard ES modulesNodeNext— Node.js’s mix of ES modules + CommonJS
If you use a bundler (Vite, Webpack), almost always ESNext. For pure Node.js projects, NodeNext is usually the answer.
moduleResolution — how to resolve modules #
{ "compilerOptions": { "moduleResolution": "bundler" } }How TypeScript resolves import paths. Recently bundler (a new mode for bundlers like Vite/Webpack) or NodeNext is recommended.
lib — available built-in APIs #
{ "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"] } }The built-in library types TypeScript should know about. If you run in browsers, include DOM; for Node-only, leave it out and use @types/node.
outDir / rootDir — input/output paths #
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
}
}rootDir— the source root (where to start compiling)outDir— where output files go
If you specify outDir, the compiled .js files collect there. Without it, they’re created next to the .ts files and clutter the folder.
include / exclude — which files to compile #
{
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}include lists what to compile, exclude lists folders to skip. node_modules is almost always in exclude.
sourceMap — source maps for debugging #
{ "compilerOptions": { "sourceMap": true } }Generates extra .js.map files so browser/Node debuggers can map back to your original .ts files. Debugging gets much easier.
jsx — for React projects #
{ "compilerOptions": { "jsx": "react-jsx" } }Decides how JSX is compiled. For modern React, react-jsx; for Next.js, preserve (Next.js handles it itself).
Putting it together — a modern recommended config #
For a new project, the following is a solid starting point.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"declaration": true,
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Other options worth knowing:
noUncheckedIndexedAccess— adds| undefinedto array index access.arr[0]becomesT | undefinedinstead ofT. Big safety win, sometimes annoying. Recommend turning on if you can take it.esModuleInterop— improves CommonJS/ES module interop. Almost always true.skipLibCheck— skips checking external library.d.tsfiles. Faster compiles. Almost always true.declaration— also generates.d.tsfiles. Needed when publishing a library.noEmit— if true, doesn’t emit compile output. Used for type-checking only (when another tool like Vite does the transpile).
A Vite project doesn’t have tsc transpile — only type-check — so it’ll likely have noEmit: true.
Looking back at the series #
In this series we covered:
| # | Topic | Core |
|---|---|---|
| 1 | Getting started and setup | TS motivation, compile flow, first code |
| 2 | Basic types | string/number/boolean/array/tuple/object/enum, any/unknown |
| 3 | interface and type alias | naming object types, the differences between the two tools |
| 4 | union/literal/narrowing | expressing multiple possibilities + type narrowing |
| 5 | Function types | optional/default/rest, overloads, intro to generics |
| 6 | Generics in depth | constraints, keyof, indexed access, conditional types |
| 7 | Utility types + tsconfig | standard utilities, compile setup |
Learning TypeScript isn’t about absorbing everything in one pass — it’s an area where familiarity grows as you write code. With this series you’ve met the names and shapes of nearly every tool, and you can now think “ah, I need that narrowing pattern from #4” when you get stuck while writing code.
Next steps #
Tackle these right away #
- Migrate one of your small JavaScript projects to TS — rename
.jsto.tsand fix errors one at a time - Start new projects in TS from scratch — pick the TS option in Vite or Next.js
- Read a library’s
.d.tsfile — openingnode_modules/lodash/index.d.tsshows real-world types in practice
What you’ll need soon #
@types/...packages — adds types to libraries that don’t ship themZodorValibot— runtime validation libraries. Combine with TS for higher safety.- TypeScript ESLint — lint rules (e.g.,
no-explicit-any,prefer-const)
Follow-up series — React + TypeScript #
The just-finished React series of 31 posts is all in JavaScript. Next, the React + TypeScript series covers how to express all those patterns inside TS — props typing, hook typing, generic components, polymorphism. The patterns you use most in real work.
Wrap-up #
Thank you for following along through 7 posts. TypeScript has a steep initial curve, but cross a certain threshold and it becomes a tool you can’t go back from. The value of autocomplete, refactoring safety, and immediate feedback is hard to forget once experienced.
If there’s one thing to remember — it’s the habit of declaring your data shapes. Just the habit of writing function signatures, object types, and API response shapes explicitly at the top of your code makes you write better code. Types are ultimately a tool to make you think more deeply about your code.
Go back to a small project you’d like to build, and this time start with TypeScript. Real learning happens where you get stuck.