Home > Back-end >  How to manage Typescript conditioning interfaces?
How to manage Typescript conditioning interfaces?

Time:12-23

I have a case when API returns 2 different types of history logs.

The interface for the operation was successful.

export interface IsyncSuccess { 
    deleted_count: number,
    new_count: number
} 

And interface for operation failed

export interface IsyncFailed { 
    error_message: string
}

Interface for entire log.

export interface logType {
    type: "SYNC_SUCCESS" | "SYNC_FAILED" // for simplicity here are strings, normally its enum
    data: IsyncSuccess | IsyncFailed
}

And in table row I have conditioning like that

const HistoryTableRow: React.FC<{ log: logType }> = React.memo(({ log }) => {
    const getLog = () => {
        if (log.type === "SYNC_SUCCESS") {
            const data = log.data as IsyncSuccess
            return <>
                <Td>
                    {data.new_count}
                </Td>
                <Td>
                    {data.deleted_count}
                </Td>
            </>
        } else {
            const data = log.data as IsyncFailed
            return <>
                <Td colspan={2}>
                    {data.error_message}
                </Td>
            </>
        }
    }
    return (
        <TbodyTr>
            {getLog()}
        </TbodyTr>
    )
}, (prevProps, nextProps) => (true))

It works. Is good practice conditioning interfaces? Is there more elegant solution for this case, when API returns different log types?

CodePudding user response:

A discriminated union type may be more appropriate here as a way of combining your possible states in a way that will also give you type-safety/intellisense. For example:

export interface ISyncSuccess { 
    deleted_count: number,
    new_count: number
}
export interface ISyncFailed { 
    error_message: string
}

interface ILogTypeSuccess {
  type: "SYNC_SUCCESS"
  data: ISyncSuccess
}
interface ILogTypeError {
  type: "SYNC_ERROR"
  data: ISyncFailed
}
type LogType = ILogTypeSuccess | ILogTypeError

You can then use the type property to "discriminate" and infer what type the data property will be (with the original approach data would always be of type ISyncSuccess | ISyncFailed regardless of the type of type)

function checkLogType(logType: LogType) {
    if (logType.type === "SYNC_SUCCESS") {
        logType.data // ISyncSuccess
    }
    else {
        logType.data // ISyncError
    }
}

const validSuccess: LogType = {
    type: "SYNC_SUCCESS",
    data: {
        deleted_count: 0,
        new_count: 0,
    }
}
const invalidSuccess: LogType = {
    type: "SYNC_SUCCESS",
    data: {
        error_message: 'some message' // Error: Object literal may only specify known properties, and 'error_message' does not exist in type 'ISyncSuccess'.(2322)
    }
}

It is also easy to add new states in without losing that benefit, e.g.

interface ISyncLoading {
  loading_message: string;
}
interface ILogTypeLoading {
  type: "SYNC_LOADING"
  data: ISyncLoading
}
type LogType = ILogTypeSuccess | ILogTypeError | ILogTypeLoading

const validLoading: LogType = {
    type: "SYNC_LOADING",
    data: {
        loading_message: 'some message'
    }
}

TypeScript Playground link

  • Related