Skip to main content

Use generic instead of `any`

When to use

Required general function for covering all type.
Suppose return itself as it is.

function getNumberFirstElement(arr: number[]): number {
return arr[0];
}
function getStringFirstElement(arr: string[]): string {
return arr[0]
}
// ..

You should have write handy code with all types you need.

Why to use

There's any type in typescript.
But it doesn't care about that data type changed in function (e.g. request type and response type is not matched)

function getNumberFirstElement(arr: any[]): any {
// No error whatever something type
// e.g. number to string
return arr[0].toString()
}

How to use

Declare function with T keyword.

// higlihght-next-line
function returnItSelf<T>(something: T): T {
// Ensure request type and reesponse type is same.
return something
}

describe('Generics', ()=>{
test('generic function call', ()=>{
const str = 'Khazix'
expect(typeof str).toBe('string')
expect(typeof returnItSelf(str)).toBe('string')

const num = 5
expect(typeof num).toBe('number')
expect(typeof returnItSelf(num)).toBe('number')
})
})

You get reusable code using generic type.
Make it easier to write modular code that can be used in different contexts.
However, take care about overuse of generic types can make code more complex and harder to read.
It's important to get balance between flexibility and simplicity.

Type inference in Generic

Typescript compiler supports type inference at generic type function from input type.
It's enough smart to infer type.

pickRandomElement.test.ts

describe('Generics', ()=> {
function pickRandomElement<T>(arr: T[]): T {
const randomIdx = Math.floor(Math.random() * arr.length)
return arr[randomIdx]
}

test('Pick random number from array',()=>{
const nums = [1, 6, 3, 4]
const randomNum = pickRandomElement(nums)
// Typescript compiler already knows typeof randomNum is number
// also knows input type is number[]
randomNum.toExponential(3)
})

test('Pick random string from array', ()=>{
const strs = ['khazix', 'rumble', 'jayce']
const str = pickRandomElement(strs)
// Typescript compiler is smart!
str.toLowerCase()
})
})

mergeObject.test.ts

describe('Generic2',()=>{
// Notify type and return type to typescript compiler.
function mergeObj<T,U>(obj1: T, obj2: U): T & U {
return {
...obj1,
...obj2
}
}

interface RGB {
R: number
G: number
B: number
}
interface Circle {
radius: number
}

test('Merge any type object', ()=>{
const rgb : RGB = {R: 221, G: 15, B: 93}
const circle : Circle = {radius: 4}
// TSC let you know what's the return type of `mergeObj()` by type inference
// since Generic <T, U> is used to define function.
const drawing = mergeObj(rgb, circle)

expect(drawing.R).toBe(rgb.R)
expect(drawing.G).toBe(rgb.G)
expect(drawing.B).toBe(rgb.B)
expect(drawing.radius).toBe(circle.radius)
})
})

constraintGeneric.test.ts

describe('Restrict generic parameter with extends', ()=>{
// Restrict only object parameter on function
function mergeObj<T extends Object,U extends Object>(obj1: T, obj2: U): T & U {
return {
...obj1,
...obj2
}
}

test('merge: primitive types should not be spread except string', ()=>{
expect(mergeObj({name: 'khazix'}, 4)).toStrictEqual({name: 'khazix'})
expect(mergeObj({name: 'khazix'}, false)).toStrictEqual({name: 'khazix'})
expect(mergeObj({name: 'khazix'}, null)).toStrictEqual({name: 'khazix'})
expect(mergeObj({name: 'khazix'}, 'str')).not.toStrictEqual(({name: 'khazix'}))
})
})

constraintGeneric2.test.ts

describe('Restrict generic parameter with extends or interface', ()=> {
// When to use type generic and when to use interface on parameter?
// More generic code, and modifiable
// However, it's not easy to maintain the code
function printDoubleLength<T extends Lengthy>(thing: T): number {
return thing.length * 2
}

interface Lengthy {
length: number
}


function printDoubleLengthWithInterface(thing: Lengthy): number {
return thing.length * 2
}


test('Constraint with generic', () => {
const str = 'sdt'
expect(printDoubleLength(str)).toBe(str.length * 2)
})

test('Constraint with interface', ()=>{
const lengtyd = {
length: 7,
age: 23
}
expect(printDoubleLengthWithInterface(lengtyd)).toBe(lengtyd.length * 2)
})
})

How to decide to use generic with extends or interface?

tip

I usually recommend interface for maintenance.

Generic type function is not easy to maintain even if it's a little more flexible.
However, when to make library code, you could consider using generic with extends.

Default type allocation

Default type allocation as shown below.
If use bracket on general function, type allocation occurs, if don't, default type is allocated.

// Default type is string <T = string>
function makeEmptyList<T = string>(): T[] {
return []
}

const strs = makeEmptyList()
// Just allowed push 'string'
strs.push('5')

const numbers = makeEmptyList<number>()
// Just allowed to push 'number'
numbers.push(5)