1. Generic class for API requests
4. React, return component type
1. Bit representation of object
Let's assume that we have next allowed endpoints:
const enum Endpoints {
/**
* Have only GET and POST
*/
users = "/api/users",
/**
* Have only POST and DELETE
*/
notes = "/api/notes",
/**
* Have only GET
*/
entitlements = "/api/entitlements",
}
You might have noticed, that I used const enum
instead of enum
. This technique will reduce you code output.
Please, keep in mind, it works bad with babel.
You allowed to make:
GET
|POST
requests for/users
POST
|DELETE
requests for/notes
GET
requests for/entitlements
Let's define interfaces of our allowed fetch methods:
interface HandleUsers {
get<T>(url: Endpoints.users): Promise<T>;
post(url: Endpoints.users): Promise<Response>;
}
interface HandleNotes {
post(url: Endpoints.notes): Promise<Response>;
delete(url: Endpoints.notes): Promise<Response>;
}
interface HandleEntitlements {
get<T>(url: Endpoints.entitlements): Promise<T>;
}
Now, we can define our main API
class
class Api {
get = <T = void>(url: Endpoints): Promise<T> =>
fetch(url).then((response) => response.json());
post = (url: Endpoints) => fetch(url, { method: "POST" });
delete = (url: Endpoints) => fetch(url, { method: "DELETE" });
}
For now, class Api
does not have any constraints.
Let's define them:
// Just helper
type RequiredGeneric<T> = T extends void
? { __TYPE__ERROR__: "Please provide generic parameter" }
: T;
interface HandleHttp {
<T extends void>(): RequiredGeneric<T>;
<T extends Endpoints.users>(): HandleUsers;
<T extends Endpoints.notes>(): HandleNotes;
<T extends Endpoints.entitlements>(): HandleEntitlements;
}
As You see, HandleHttp
is just overloading for function. Nothing special except the first line. I will come back to it later.
We have class Api
and overloadings for function. How we can combine them? Very simple - we will just create a function which returns instance of Api class
const handleHttp: HandleHttp = <_ extends Endpoints>() => new Api();
Take a look on generic parameter of httpHandler
and HandleHttp
interface, there is a relation between them.
Let's test our result:
const request1 = handleHttp<Endpoints.notes>(); // only delete and post methods are allowed
const request2 = handleHttp<Endpoints.users>(); // only get and post methods are allowed
const request3 = handleHttp<Endpoints.entitlements>(); // only get method is allowed
If you have forgotten to provide generic parameter, return type of request
will be
const request = {
__TYPE__ERROR__: 'Please provide generic parameter';
}
Drawbacks:
- Without generic parameter, using
request.TYPE _ERROR
will be perfectly valid from TS point of view Api
class is not singleton, you should create it every time
2.Math operations - link
Let's assume, You want to make some math operations either on number or bigint
Please keep in mind, this code is not valid in JS/TS:
const sum = 2 + 2n; // Error
So, we want to accept only number or only bigints. Let's start with function definition:
function sum<A extends number | bigint>(a: A, b: A) {
return a * a + b * b;
}
Unfortunately, this function don't work as expected. Let's test it:
const x = 3n;
let y: number | bigint;
if (Math.random() < 0.5) y = 4;
else y = 4n;
sum(x, y); // OK
In above case, y
can be either number
or bigint
. So, from TS point of view it is ok, but I'd willing to bet,
that it will throw at least 1 error in dev environment and 1K errors in production. It was a joke.
Ok, what we can do? We can define two generic parameters:
type Numbers = number | bigint;
function sumOfSquares<A extends Numbers, B extends Numbers>(
a: A,
b: B
): Numbers {
return a * a + b * b;
}
const result = sumOfSquares(x, y); // There should be an error
const result2 = sumOfSquares(3n, 4); // There should be an error
const result3 = sumOfSquares(3, 4n); // There should be an error
const result4 = sumOfSquares(3, 4); // ok
const result5 = sumOfSquares(3n, 4n); // ok
Unfortunately, above example still don't work as we expect.
<A extends Numbers, B extends Numbers>
// same as
<A extends number | bigint, B extends number | bigint>
// so A can be number and B in the same time can be bigint
Only overloadings might help us here. We should explicitly say, that B
generic parameter should have same type as A
type Numbers = number | bigint;
function sumOfSquares<A extends number, B extends number>(a: A, b: B): number;
function sumOfSquares<A extends bigint, B extends bigint>(a: A, b: B): bigint;
function sumOfSquares<A extends Numbers, B extends A>(a: A, b: B): Numbers {
return a * a + b * b;
}
const result = sumOfSquares(x, y); // There should be an error
const result2 = sumOfSquares(3n, 4); // There should be an error
const result3 = sumOfSquares(3, 4n); // There should be an error
const result4 = sumOfSquares(3, 4); // ok
const result5 = sumOfSquares(3n, 4n); // ok
3.Typed React children - link
Let's assume you want to create component which will accept array of children components with certain props ? First approach:
function One() {
return <div>Child One</div>;
}
const Two: React.FC<{ label: string }> = ({ label }) => {
return <div>{label}</div>;
};
function Parent({ children }: { children: JSX.Element[] }) {
return (
<div>
{children.map((child) => (
<div key={child.props.label}>{child}</div> // Props: any
))}
</div>
);
}
function App2() {
return <Parent children={[<One />, <Two label={"hello"} />]} />;
}
As you see, there is nothing highlighted, code is ok, but we still need to accept components with label
props.
Let's change out children
prop type.
function One() {
return <div>Child One</div>;
}
const Two: React.FC<{ label: string }> = ({ label }) => {
return <div>{label}</div>;
};
function Parent({
children,
}: {
children: React.ReactElement<{ label: string }>[]; // change is here
}) {
return (
<div>
{children.map((child) => (
<div key={child.props.label}>{child}</div>
))}
</div>
);
}
function App2() {
return <Parent children={[<One />, <Two label={"hello"} />]} />;
}
Does it work - no! But, why????
Because React.ReactElement<{label: string}>
is still union type, and in fact, it accepts (props:any)=>{}
It is looks like, we all forgot about React native syntax, did not we?
On my second approach, I will change a bit declaration of One
component.
// One is explicitly typed now
const One: React.FC = () => {
return <div>Child One</div>;
}
const Two: React.FC<{ label: string }> = ({ label }) => {
return <div>{label}</div>;
};
function Parent({
children
}: {
children: React.ReactElement<{ label: string }>[];
}) {
return (
<div>
{children.map((child) => (
<div key={child.props.label}>{child}</div>
))}
</div>
);
}
function App2() {
return React.createElement(Parent, {
children: [
// error, all components should have label prop
React.createElement(One, null),
React.createElement(Two, { label: 'd' }) // no error
]
});
}
function App3() {
return React.createElement(Parent, {
children: [
/**
* still error, because One don't expect {label: string}
* If you add typings for One, error will disappear
*/
React.createElement(One, { label: 'd' }), //error
React.createElement(Two, { label: 'd' }) // no error
]
})
So, we can write now the code which will meet our requirements:
const One: React.FC<{ label: string }> = ({ label }) => {
return <div>{label}</div>;
};
const Two: React.FC<{ label: string }> = ({ label }) => {
return <div>{label}</div>;
};
function Parent({
children,
}: {
children: React.ReactElement<{ label: string }>[];
}) {
return (
<div>
{children.map((child) => (
<div key={child.props.label}>{child}</div>
))}
</div>
);
}
function App4() {
return React.createElement(Parent, {
children: [
React.createElement(One, { label: "d" }),
React.createElement(Two, { label: "d" }),
],
});
}
4.React component return type - link
There is a common pattern for typing return value of component:
type Props = {
label: string;
name: string;
};
const Result: FC<Props> = (prop: Props): JSX.Element => <One label={"hello"} />;
type ComponentReturnType = ReturnType<typeof Result>; // React.ReactElement<any, any> | null
Is it correct ? - Yes.
Is it helpful ? - Not really.
What if you need to make sure that Result component will always return component with some particular props.
For example I'm interested in {label:string}
property.
type Props = {
label: string;
};
type CustomReturn = React.ReactElement<Props>;
const MainButton: FC<Props> = (prop: Props): CustomReturn => <Two />;
Unfortunately, there is no error. This code compiles. Native React syntax comes to help!
const Two: React.FC = () => <div></div>;
type Props = {
label: string;
};
type CustomReturn = React.ReactElement<Props>;
const MainButton: FC<Props> = (prop: Props): CustomReturn =>
React.createElement(Two); // Error
Finally, we have an error:
Type 'FunctionComponentElement<{}>' is not assignable to type 'CustomReturn'. Types of property 'props' are incompatible.
Property 'label' is missing in type '{}' but required in type '{ label: string; }'.ts(2322)
This code works as expected:
type Props = {
label: string;
};
type CustomReturn = React.ReactElement<Props>;
const One: React.VFC<{ label: string }> = ({ label }) => <div>{label} </div>;
const MainButton: FC<Props> = (props: Props): CustomReturn =>
React.createElement(One, props);
Btw, small reminder, how to use generics with React components:
import React from "react";
type Props<D, S> = {
data: D;
selector: (data: D) => S;
render: (data: S) => any;
};
const Comp = <D, S>(props: Props<D, S>) => null;
const result = (
<Comp<number, string>
data={2}
selector={(data: number) => "fg"}
render={(data: string) => 42}
/>
); // ok
const result2 = (
<Comp<number, string>
data={2}
selector={(data: string) => "fg"}
render={(data: string) => 42}
/>
); // expected error
const result3 = (
<Comp<number, string>
data={2}
selector={(data: number) => "fg"}
render={(data: number) => 42}
/>
); // expected error
5.Compare arguments in TypeScript - link
Let's say you want to make a function with next constraints:
- First argument should be an array
- Second arguments should be 2D array, where each nested array has same length as first argument Pseudocode:
const handleArray=(x: number[], y:number[][])=>void
Let's start from defining our function. This is very naive approach, we will improve it later
type ArrayElement = number;
type Array1D = ReadonlyArray<ArrayElement>;
function array<X extends Array1D, Y extends readonly ArrayElement[]>(
x: X,
y: readonly Y[]
) {}
const result = array([1, 2, 3], [[1, 1, 1, 1, 1]]); // no error, unexpected behaviour
So, now we should find a way to compare length of first argument with length of all inner arrays of second argument.
First, of all we should define Length
util.
/**
* Get length of the array
*/
type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? L
: never;
/**
* There is another approach to get the length of the array
*/
type Length2<T extends ReadonlyArray<any>> = T["length"];
type ArrayMock1 = readonly [1, 2, 3];
type ArrayMock2 = readonly [4, 5, 6, 7];
type ArrayMock3 = readonly [8, 9, 10];
type ArrayMock4 = readonly [[11, 12, 14]];
type ArrayMock5 = readonly [[15, 16]];
type ArrayMock6 = readonly [[1], [2], [3]];
type TestArrayLength1 = Length<ArrayMock1>; // 3
type TestArrayLength2 = Length<ArrayMock2>; // 4
Now, when we know how to get length of the array, we should create comparison util.
/**
* Compare length of the arrays
*/
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = Length<X> extends Length<Y> ? true : false;
/**
* Tests
*/
type TestCompareLength1 = CompareLength<ArrayMock1, ArrayMock2>; // false
type TestCompareLength2 = CompareLength<ArrayMock1, ArrayMock3>; // true
It is looks like we have all our necessary utils. If you still have't head ake, here You have other portion of types to think about. Try to figure out whats going on here:
{
type Foo = {
x: number;
};
type FooX = {
x: number;
}["x"];
type Result = FooX extends number ? true : false; // true
const obj = {
y: 42,
}["y"];
obj; // 42
}
/**
* A bit complicated with weird conditional statement
*/
{
type Foo = {
x: number;
};
type FooX<T> = {
x: number;
y: string;
}[T extends number ? "x" : "y"];
type Result = FooX<number> extends number ? true : false; // true
type Result2 = FooX<string> extends string ? true : false; // true
const condition = 2;
const obj = {
y: 42,
x: 43,
}[condition === 2 ? "y" : "x"];
obj; // 42
}
Now, when You are familiar with such a weird syntax, we can go further. Here is our function definition with 1 overloading.
function array<
X extends Array1D,
Y extends {
0: readonly ArrayElement[];
}[CompareLength<X, Y> extends true ? 0 : never]
>(x: X, y: readonly Y[]): void;
function array<X extends Array1D, Y extends readonly ArrayElement[]>(
x: X,
y: readonly Y[]
) {}
And here is our whole code placed in one block with type tests
type ArrayElement = number;
type Array1D = ReadonlyArray<ArrayElement>;
type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? L
: never;
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = Length<X> extends Length<Y> ? true : false;
function array<
X extends Array1D,
Y extends {
0: readonly ArrayElement[];
}[CompareLength<X, Y> extends true ? 0 : never]
>(x: X, y: readonly Y[]): void;
function array<X extends Array1D, Y extends readonly ArrayElement[]>(
x: X,
y: readonly Y[]
) {}
const arr = [1, 2, 3] as const;
/**
* No errors expected
*/
const result = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2, 3],
] as const
); // ok
const result0 = array([1, 2, 3] as const, [[1, 1, 1]] as const); // ok
/**
* Errors expected
*/
const result1 = array([1, 2, 3], [[1, 1, 1], [1]]); // no error, but should be
const result2 = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2],
] as const
); // no error, but should be
const result3 = array([1, 2, 3] as const, [[1, 2]] as const); // error
const result5 = array([1, 2, 3] as const, [1] as const); // error
const result6 = array([1, 2, 3] as const, [[1, 2, 3], []] as const); // no error, but should be
const result7 = array(arr, [[1, 1, 1]]); // no error, but should be
It is look like we made logical error somewhere in the code. Ok, not you, I made :D.
Maybe we should test again our Length
type util. What exactly are we expect from this util ? Answer is - literal integer. Let's test it again:
type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? L
: never;
const array1 = [1, 2, 3];
type Test1 = Length<[1, 2, 3]>; // ok - 3 literal type
type Test2 = Length<typeof array1>; // not ok - number
/**
* Q: But why TS does not complain here?
* I have explicitly defined that Length argument should extend ReadonlyArray
*
* A: Because, literal array type extends ReadonlyArray
* When you use `typeof array1`, TS treats it as number[], because array1 is mutable.
* Hence TS can't infer the length of array1. It is possible only in runtime
*/
So, we should provide extra restrictions for mutable arrays? Correct? - Yes. Let's provide them:
// Type of mutable array length will be always - number
type MutableLength = unknown[]["length"]; // number
type CheckCompatibility = number extends 5 ? true : false; // false
type CheckCompatibility2 = 5 extends number ? true : false; // true
/**
* This code works because number does not extends literal 5
* but literal 5 does extends number
*/
export type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? MutableLength extends L
? MutableLength
: L
: MutableLength;
/**
* Tests
*/
const array = [1, 2, 3];
const arrayImm = [1, 2, 3] as const;
type Test1 = Length<number[]>; // number
type Test2 = Length<unknown[]>; // number
type Test3 = Length<typeof array>; // number
type Test4 = Length<typeof arrayImm>; // 3
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = Length<X> extends Length<Y> ? true : false;
/**
* Let's test again CompareLength
*/
const arr1 = [1, 2, 3];
const arr2 = [1, 2];
type Test1 = CompareLength<typeof arr1, typeof arr2>; // true, BANG! this is not what we are expect!
CompareLength
should be rewritten as follow:
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = MutableLength extends Length<X>
? false
: MutableLength extends Length<Y>
? false
: Length<X> extends Length<Y>
? true
: false;
/**
* Tests
*/
const arr1 = [1, 2, 3];
const arr2 = [1, 2];
type Test1 = CompareLength<typeof arr1, typeof arr2>; // false, expected
Ok, I'm exhausted now, it should work. Let's test our code:
type ArrayElement = number;
type Array1D = ReadonlyArray<ArrayElement>;
type MutableLength = unknown[]["length"]; // number
/**
* Get length of the array
* Allow only immutable arrays
*/
export type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? MutableLength extends L
? MutableLength
: L
: MutableLength;
/**
* Compare length of the arrays
*/
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = MutableLength extends Length<X>
? false
: MutableLength extends Length<Y>
? false
: Length<X> extends Length<Y>
? true
: false;
/**
* CompareLength, compares length of X and filtered Y,
* if true - return zero index element - ReadonlyArray<ArrayElement>
* if false - return never
*
* So, if it will return never, then you can't pass second argument,
* but if you did not provide second argument, you will receive another error -
* function expects two arguments
*/
function array<
X extends Array1D,
Y extends {
0: readonly ArrayElement[];
}[CompareLength<X, Y> extends true ? 0 : never]
>(x: X, y: readonly Y[]): "put here your returned type";
function array<
X extends Array1D,
Y extends readonly ArrayElement[],
Z extends CompareLength<X, Y>
>(x: X, y: readonly Y[]) {
return [1, 2, 3] as any;
}
const result = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2, 3],
] as const
); // ok
const result0 = array([1, 2, 3] as const, [[1, 1, 1]] as const); // ok
const arr = [1, 2, 3] as const;
const result1 = array([1, 2, 3], [[1, 1, 1], [1]]); // error
const result2 = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2],
] as const
); // no error, but SHOULD BE
const result3 = array([1, 2, 3] as const, [[1, 2]] as const); // error
const result5 = array([1, 2, 3] as const, [1] as const); // error
const result5 = array([1, 2, 3] as const, [[1, 2, 3], []] as const); // error
const result6 = array(arr, [[1, 1, 1]]); // error, because TS is unable to fidure out length of mutable array.
Ohh, what a .... What's going on here ? We still need to fix one failed test, see result3
.
It looks like if second arguments contains at least one array which feet requirements,
TS ok with it. So we should compare arrays only if their length are equal.
/**
* Check if all inner arrays have same length as X
*/
type Filter<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = X["length"] extends Y["length"]
? Y["length"] extends X["length"]
? Y
: never
: never;
type Test1 = Filter<[1, 2, 3], [1, 2]>; // never
type Test2 = Filter<[], [1, 2]>; // never
type Test3 = Filter<[1, 2], [1, 2]>; // [1,2]
/**
* Please keep in mind that Y can be and will be a union type of inner arrays
*
*/
type Test4 = Filter<[], [1, 2] | [1, 2, 3]>; // never
type Test5 = Filter<[1, 2], [1] | [1, 2] | [1, 2, 3]>; // never
/**
* Btw, try to rewrite Filter without second conditional
*/
type Filter<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = X["length"] extends Y["length"] ? Y : never;
type Test5 = Filter<[1, 2], [1] | [1, 2] | [1, 2, 3]>; // not never, not ok
/**
* Why ?
*/
type O = 5 extends 1 | 2 | 5 ? true : false; // true
type O2 = 1 | 2 | 5 extends 5 ? true : false; // false
Finall working code:
type ArrayElement = number;
type Array1D = ReadonlyArray<ArrayElement>;
type MutableLength = unknown[]["length"]; // number
export type Length<T extends ReadonlyArray<any>> = T extends { length: infer L }
? MutableLength extends L
? MutableLength
: L
: MutableLength;
type CompareLength<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = MutableLength extends Length<X>
? false
: MutableLength extends Length<Y>
? false
: Length<X> extends Length<Y>
? true
: false;
type Filter<
X extends ReadonlyArray<any>,
Y extends ReadonlyArray<any>
> = X["length"] extends Y["length"]
? Y["length"] extends X["length"]
? Y
: never
: never;
function array<
X extends Array1D,
Y extends {
0: readonly ArrayElement[];
}[CompareLength<X, Filter<X, Y>> extends true ? 0 : never]
>(x: X, y: readonly Y[]): "put here your returned type";
function array<
X extends Array1D,
Y extends readonly ArrayElement[],
Z extends CompareLength<X, Y>
>(x: X, y: readonly Y[]) {
return [1, 2, 3] as any;
}
/**
* Positive Tests
*/
const result = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2, 3],
] as const
); // ok
const result0 = array([1, 2, 3] as const, [[1, 1, 1]] as const); // ok
const arr = [1, 2, 3] as const;
/**
* Negative Tests
*/
const result1 = array([1, 2, 3], [[1, 1, 1], [1]]); // error
const result2 = array(
[1, 2, 3] as const,
[
[1, 1, 1],
[1, 2],
] as const
); // error
const result3 = array([1, 2, 3] as const, [[1, 2]] as const); // error
const result4 = array([1, 2, 3] as const, [[1], [1, 2], [1, 2, 3]] as const); // error
const result5 = array([1, 2, 3] as const, [1] as const); // error
const result6 = array([1, 2, 3] as const, [[1, 2, 3], []] as const); // error
const result7 = array(arr, [[1, 1, 1]]); // error, because TS is unable to figure out length of mutable array.
6. Generate numbers in range - link
Let's take a look on type Values = T[keyof T]
utility.
Maybe you wonder, what does it mean ?
Before we continue, please make sure you are aware of distibutive types
Let's start with simple example:
interface Foo {
age: number;
name: string;
}
type Alias1 = Foo["age"]; // number
type Alias2 = Foo["name"]; // stirng
type Alias3 = Foo["age" | "name"]; // string | number
type Check = keyof Foo; // 'age'|'name
Our Values
utility works perfect with objects, but not with arrays.
To get all keys of object, we use - keyof
.
To get all array elements we use [number]
because arrays have numeric keys.
type Arr = [1, 2, 3];
type Val1 = Arr[0]; // 1
type Val2 = Arr[1]; // 2
type Val3 = Arr[0 | 1]; // 1|2
type Val4 = Arr[0 | 1 | 2]; // 3 | 1 | 1
type Val5 = Arr[number]; // 3 | 1 | 1
Now, we can keep going. Let's define out utility types. For clarity, I will use simple Assert test type
type Assert<T, U extends T> = T extends U ? true : false;
type Values<T> = T[keyof T];
{
type Test1 = Values<{ age: 42; name: "John" }>; // 42 | "John"
type Test2 = Assert<Test1, "John" | 42>;
}
type LiteralDigits = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type NumberString<T extends number> = `${T}`;
{
type Test1 = Assert<NumberString<6>, "6">; // true
type Test2 = Assert<NumberString<42>, "42">; // true
type Test3 = Assert<NumberString<6>, 6>; // false
type Test4 = Assert<NumberString<6>, "6foo">; // false
}
type AppendDigit<T extends number | string> = `${T}${LiteralDigits}`;
{
/**
* If you don't understand why, please read again about distributivenes here
* https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
*/
type Test1 = Assert<
AppendDigit<2>,
"20" | "21" | "22" | "23" | "24" | "25" | "26" | "27" | "28" | "29"
>; // true
type Test2 = Assert<
AppendDigit<9>,
"90" | "91" | "92" | "93" | "94" | "95" | "96" | "97" | "98" | "99"
>; // true
}
type MakeSet<T extends number> = {
[P in T]: AppendDigit<P>;
};
{
type Test1 = Assert<
MakeSet<1>,
{
1: "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19";
}
>;
type Test2 = Assert<
MakeSet<1 | 2>,
{
1: "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19";
2: "20" | "21" | "22" | "23" | "24" | "25" | "26" | "27" | "28" | "29";
}
>;
}
type RemoveTrailingZero<
T extends string
> = T extends `${infer Fst}${infer Rest}`
? Fst extends `0`
? RemoveTrailingZero<`${Rest}`>
: `${Fst}${Rest}`
: never;
{
/**
* Because nobody uses 01 | 02 ... | 0n
* Everybody use 1 | 2 | 3 ... | n
*/
type Test1 = Assert<RemoveTrailingZero<"01">, "1">;
type Test2 = Assert<RemoveTrailingZero<"02" | "03">, "2" | "3">;
type Test3 = Assert<RemoveTrailingZero<"002" | "003">, "2" | "3">;
}
type From_1_to_999 = RemoveTrailingZero<
Values<
{
[P in Values<MakeSet<LiteralDigits>>]: AppendDigit<P>;
}
>
>;
type By<V extends NumberString<number>> = RemoveTrailingZero<
Values<
{
[P in V]: AppendDigit<P>;
}
>
>;
/**
* Did not use recursion here,
* because my CPU will blow up
*/
type From_1_to_99999 =
| From_1_to_999
| By<From_1_to_999>
| By<From_1_to_999 | By<From_1_to_999>>;
If you still want to generate literal numbers instead of string numbers, you can use this code:
type PrependNextNum<A extends Array<unknown>> = A["length"] extends infer T
? ((t: T, ...a: A) => void) extends (...x: infer X) => void
? X
: never
: never;
type EnumerateInternal<A extends Array<unknown>, N extends number> = {
0: A;
1: EnumerateInternal<PrependNextNum<A>, N>;
}[N extends A["length"] ? 0 : 1];
type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[]
? E
: never;
// Up to 42 - meaning of the life
type Result = Enumerate<43>; // 0 | 1 | 2 | ... | 42
Please take a look on next example and answer a question: Is your generic is helpful?
const makeGenericArray = <T>(arr: Array<T>) => arr;
const colors = makeGenericArray(["red", "green", "blue"]); //type string[]
const colors2 = makeGenericArray([1, 2, 3]); //type number[]If your answer is: Yes, but actually - no, we are on the same boat
TypeScript doing his best to narrow types and made them helpful. So if you are expect T parameter to be either string or number, please provide constraints:
const makeStringArray = <T extends string | number>(arr: Array<T>) => arr;
const colorsLiteral = makeStringArray(["red", "green", "blue"]); // type ("red" | "green" | "blue")[]
const colorsLiteral2 = makeStringArray([1, 2, 3]); // type ("red" | "green" | "blue")[]
Now, your types are more helpful.
Simple example:
type Immutable<T> = { readonly [K in keyof T]: Immutable<T[K]> };
function foo<T>(t: T): Immutable<T> {
return t;
}
const result1 = foo({ age: { name: "John" } }); // { readonly age: { name: string }; }
Make all properties immutable except name children
type Immutable<T> = {
readonly [K in keyof T]: K extends "name" ? T[K] : Immutable<T[K]>;
};
function foo<T>(t: T): Immutable<T> {
return (t as any) as Immutable<T>;
}
const result1 = foo({ age: { name: { surname: "Doe" } } });
result1.age.name = "2"; // error
result1.age.name.surname = "2"; // ok
10. Typeguards - link
I'd willing to bet, you are using Array.prototype.filter 1 hundred times per day. And I bet you want to do it like a PRO. I have found this example in this book Let's say you have an array, and you want to get rid of 4s and 9s
const arr = [85, 65, 4, 9] as const;
type Arr = typeof arr;
/**
* Naive approach
*/
const result_naive = arr.filter((elem) => elem !== 4 && elem !== 9); // (85 | 65 | 4 | 9)[]
You still have 4 | 9 in your union type. This is not what we expected. Here is much better approach:
type Without_4_and_9 = Exclude<Arr[number], 4 | 9>;
const result = arr.filter(
(elem): elem is Without_4_and_9 => elem !== 4 && elem !== 9
); // (85 | 65)[]
By using simple utility types, we can emulate JS Array.prototype.findIndex
const arr = [85, 65, 4, 9] as const;
type Arr = typeof arr;
type Values<T> = T[keyof T];
type ArrayKeys = keyof [];
type FindIndex<
T extends ReadonlyArray<number>,
Value extends number = 0,
Keys extends keyof T = keyof T
> = {
[P in Keys]: Value extends T[P] ? P : never;
};
type Result = Values<Omit<FindIndex<Arr, 65>, ArrayKeys>>; // '1'
Is second example is useful in practive? Of course not) Will it help you to understand better TS type system? - Definitely Here you can find very interesting example with typeguards:
11. Handle tuples - link
Let's say you have a literal type of array and you want to filter this type
/**
* We should get rid of all numbers
*/
type Arr = [number, string, ...number[]];
type Filter<T, U>= // @todo
// Our test suite
type Test0 = Filter<[], number>; // []
type Test1 = Filter<Arr, number>; // [string, ...number[]]
type Test2 = Filter<Arr, number[]>; // [number, string]
type Test3 = Filter<Arr, number | number[]>; // [string]
type Test4 = Filter<Arr, number | number[] | string>; // []
First of all, we should handle all possible cases. What if first generic parameter of Filter
is empty array?
type Filter<T extends any[], F> = T extends [] ? [] : T; //
type Test0 = Filter<[], number>; // []
Before, we continue, make sure you are aware of some basics of functional programming lists operations.
Each list has Head - the first element and Tail - all elements but first.
To iterate recursively through the tuple, we should every time to separate Head
and Tail
type Head<T extends ReadonlyArray<unknown>> = T extends readonly [
infer H,
...infer Tail
]
? H
: never;
{
type Test1 = Assert<Head<[1, 2, 3]>, 1>;
type Test2 = Assert<Head<["head", "tail"]>, "head">;
type Test3 = Assert<Head<[]>, never>;
}
type Tail<List extends ReadonlyArray<unknown>> = List extends readonly [
infer H,
...infer Tail
]
? Tail
: never;
{
type Test1 = Assert<Tail<[1, 2, 3]>, [2, 3]>;
type Test2 = Assert<Tail<["head", "tail"]>, ["tail"]>;
type Test3 = Assert<Head<[]>, never>;
}
// Before TS 4
type Tail_before_TS_4<T extends any[]> = ((...args: T) => void) extends (
first: any,
...rest: infer S1
) => void
? S1
: T extends [infer S2]
? []
: T extends []
? []
: never;
Very straightforward. I hope you have noticed, that in case of empty list, I returned never, because empty list has not neither head nor tail Now, we can define our recursive type:
type Filter<T extends any[], F> = T extends []
? []
: T extends [infer Head, ...infer Tail]
? Head extends F
? Filter<Tail, F>
: [Head, ...Filter<Tail, F>]
: /* --> */ [];
{
type Test1 = Filter<Arr, number>; // [string, ...number[]]
type Test2 = Filter<Arr, number[]>; // [number, string]
type Test3 = Filter<Arr, number | number[]>; // [string]
type Test4 = Filter<Arr, number | number[] | string>; // []
type Test5 = Filter<[number[]], number | number[] | string>; // []
}
I hope you have noticed the -->
symbol at the and of conditional type.
It is mean, that if list has not neither Head nor Tail - return empty array.
I used empty array here instead of never, because we want to filter an array, not to get either Head or Tail
Is it possible to reuse above pattern for other cases ? Sure!
Take a look on this question
Let's say you have an array and you want to map it to other array. How to do it with type system?
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, [H, true]]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, [Head, true]]>
: Readonly<Result>;
type Result = Mapped<[1, 2, 3, 4]>; // [[1, true], [2, true], [3, true], [4, true]]
If you want to restrict maximum array (tuple) length - this is not a problem. Here you can find how to do it
type ArrayOfMaxLength4 = readonly [any?, any?, any?, any?];
Ok, ok. I know what you are thinking about. How we can reduce the array to object? Is it possible at all with typings? Sure, you can take a look on this answer
We should transform Data
type to ExpectedType
type
export const myArray = [
{ name: "Relationship", options: "foo" },
{ name: "Full name of family member as shown in passport", options: "bar" },
{ name: "Country family member lives in", options: "baz" },
] as const;
type Data = typeof myArray;
type ExpectedType = Array<{
Relationship: "foo";
"Full name of family member as shown in passport": "bar";
"Country family member lives in": "baz";
}>;
type Values<T> = T[keyof T];
type Data = typeof myArray;
type Elem = { readonly name: string; readonly options: string };
type MapObject<T extends Elem, Key extends keyof T, Val extends keyof T> = {
[P in Values<Pick<T, Key>> & string]: T[Val];
};
type Mapper<
Arr extends ReadonlyArray<Elem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends Elem
? Result & MapObject<H, "name", "options">
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? Mapper<Tail, Result & MapObject<H, "name", "options">>
: never
: never
: never;
type Result = Mapper<Data>[] extends ExpectedType ? true : false;
12. Transform union to array - link
Let's say you have a Union
, and you want to convert it to ExpectedArray
type Union = "one" | "two" | "three";
type ExpectedArray = ["one", "two", "three"];
There is a naive way to do it:
type Result = Union[]; // ('one' | 'two' | 'three')[]
type Test1 = Union extends ["one", "one", "one"] ? true : false; // true
As you see, it is not what we are looking for.
Next example is hard to understand, but you may learn smth new.
//Credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// Credits goes to ShanonJackson https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
// Converts union to overloaded function
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// Credits goes to ShanonJackson https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// Credit goes to Titian Cernicova-Dragomir https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#comment-94748994
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
// Finally me)
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
interface Person {
name: string;
age: number;
surname: string;
children: number;
}
type Result = UnionToArray<keyof Person>; // ["name", "age", "surname", "children"]
const func = <T>(): UnionToArray<keyof T> => null as any;
const result = func<Person>(); // ["name", "age", "surname", "children"]
Please keep in mind, this solution is not CPU friendly and there is no order guarantee.
Above solution has own drawback - there is only one order.
Next solution, which is shamelessly stolen from Wroclaw twitter group works much better and easier to understand
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never
? [...R, S]
: TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
interface Person {
firstName: string;
lastName: string;
dob: Date;
hasCats: false;
}
type keys = TupleUnion<keyof Person>; //
13. Make callback chain - link
Let's say you have next function
const myFn = <T>(arg: { a: (a_arg: number) => T; b: (b_arg: T) => void }) => {
// ...
};
It is simple. The argument of b
function should be return value of a
function.
Let's try if it works:
myFn({
a: (i) => ({ num: 0 }),
b: (b_arg) => {
b_arg.num;
}, // Error
});
But why we have an error here?
Honestly - I don't understand it so good to be able explain it.
I suggest you to read next articles to understand this error, maybe it even helps you.
My SO question
You should add an extra generic for b
function.
const myFn = <T>(arg: {
a: (a_arg: number) => T;
b: <U extends T>(b_arg: U) => void;
}) => {
// ...
};
myFn({
a: (a) => ({ num: 0 }),
b: (b_arg) => {
b_arg.num;
}, // Works!
});
How to opack JS regular objects into bits?
const obj = { top: true, category: 242, id: 123_456 }; // boolean and numbers, not strings
Constraints:
top
- can be either 1 or 0, flag - (T)
category
- can be from 0
to 999
, flag - (C)
id
- can be from 1
to 1_000_000
, flag - (I)
Let's start with encoding. How many bits we should allocate for ID's?
1_000_000..toString(2).length
-> we should allocate 20 bits
How many bits for category?
999..toString(2).length
-> 10
And for top? - 1 bit, because it is a boolean
TOP: 1
CATEGORY: 10
ID's: 20
Result:
T(1)-CCCCCCCCCC(10)-IIIIIIIIIIIIIIIIIIII(20)
TCCCCCCCCCCIIIIIIIIIIIIIIIIIIII -> length 31
const id_hex = (123_456).toString(16); // 1e240 // 11110001001000000
const category_hex = (242).toString(16); // f2 // 11110010
const top_hex = (1).toString(16); // 1
const result = 0x1f21e240; // 1 - f2 - 1e240, binary representation 1 111100100 0011110001001000000
And decoder:
const bits = (from, to, number) => ((1 << to) - 1) & (number >> (from - 1));
interface Active {
active: true;
disable(): Disabled;
}
interface Disabled {
active: false;
activate(): Active;
}
class ConnectionActive<T> implements Active {
active: true;
data: T;
constructor(data: T) {
this.data = data;
}
disable = () => new ConnectionDisabled<T>(this.data);
}
class ConnectionDisabled<T> implements Disabled {
active: false;
data: T;
constructor(data: T) {
this.data = data;
}
activate = () => new ConnectionActive<T>(this.data);
}
const socket = { foo: 42 };
const result = new ConnectionDisabled(socket);
You are able to call only activate
methods when connection is disabled and disable
when connection is enabled.
This pattern was inspired by these 3 articles:
The main goal here - is to make illegal states unrepresentable. This is always my first goal, when I'm trying to type smth.
Like I said in previous chapter, main goal - is to make illegal states unrepresentable. Please see next example. This pattern is your friend if you want to make event driven app (sockets, etc...)
const enum Events {
foo = "foo",
bar = "bar",
baz = "baz",
}
/**
* Single sourse of true
*/
interface EventMap {
[Events.foo]: { foo: number };
[Events.bar]: { bar: string };
[Events.baz]: { baz: string[] };
}
type Values<T> = T[keyof T];
// All credits goes here :
// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type EmitRecord = {
[P in keyof EventMap]: (name: P, data: EventMap[P]) => void;
};
type ListenRecord = {
[P in keyof EventMap]: (
name: P,
callback: (arg: EventMap[P]) => void
) => void;
};
type MakeOverloadings<T> = UnionToIntersection<Values<T>>;
type Emit = MakeOverloadings<EmitRecord>;
type Listen = MakeOverloadings<ListenRecord>;
const emit: Emit = <T>(name: string, data: T) => {};
emit(Events.bar, { bar: "1" });
emit(Events.baz, { baz: ["1"] });
emit("unimplemented", { foo: 2 }); // expected error
const listen: Listen = (name: string, callback: (arg: any) => void) => {};
listen(Events.baz, (arg /* { baz: string[] } */) => {});
listen(Events.bar, (arg /* { bar: string } */) => {});