Search code examples
typescript

Type 'unknown' is not assignable to type 'number'


I encounter Type 'unknown' is not assignable to type 'number' when trying to implement something like this:

interface Foo<V = unknown> {
    foo: (value: V) => void
}

class Bar implements Foo<number> {
    foo(v: number) {}
}

function test(foo: Foo): void {}

test(new Bar) // complains:

// Argument of type 'Bar' is not assignable to parameter of type 'Foo<unknown>'.
//   Types of property 'foo' are incompatible.
//     Type '(v: number) => void' is not assignable to type '(value: unknown) => void'.
//       Types of parameters 'v' and 'value' are incompatible.
//         Type 'unknown' is not assignable to type 'number'.(2345)

Why does it behave like this and how can I redesign this considering that test should be able to get different implementations of Foo, e.g.

class Bar2 implements Foo<string> {
    foo(v: string) {}
}
  • Type assertion:

    test(new Bar as Foo) // it doesn't complain now, but imo it's dirty and bad DX workaround
    
  • Tried to replace unknown in Foo with union type e.g. string | number | ..., but it didn't help.


Solution

  • Affectively the main issue here is unknown cannot be assigned to anything but itself and the any type.

    If you strip down your example more

    type FooFn = (value: unknown) => void
    const testFooFn: FooFn = (n: number) => {} // Error 
    

    You get a similar error to the one you get above.

    Type '(n: number) => void' is not assignable to type 'FooFn'.
      Types of parameters 'n' and 'value' are incompatible.
        Type 'unknown' is not assignable to type 'number'.
    

    One option, as suggested in the comments, is to use generics to carry over the type.

    interface Foo<V = unknown> {
      foo: (value: V) => void
    }
    
    class Bar implements Foo<number> {
      foo(v: number) {
        console.log(v)
      }
    }
    
    function test<T>(foo: Foo<T>): void {}
    
    test(new Bar()) // No Error
    

    Another option is change unknown to any. See the differences here, but typically it's better/safer to use unknown but requires more type gymnastics.

    interface Foo<V = any> {
      foo: (value: V) => void
    }
    
    class Bar implements Foo<number> {
      foo(v: number) {
        console.log(v)
      }
    }
    
    function test(foo: Foo): void {}
    
    test(new Bar()) // No Error
    

    See TSPlayground