It's a little bit of overkill, but one solution is to combine OverloadedStrings and RebindableSyntax . The RebindableSyntax extension causes all the implicit function calls that Haskell syntax uses to refer to whatever functions are in scope; for instance, integer literals use any fromIntegral , not necessarily Prelude.fromIntegral . As a side effect, Prelude is no longer implicitly imported, so you have to do that manually. As long as you do import it, there shouldn't be any issues with syntax using the wrong function implicitly (I think—I haven't actually used this technique). When combined with OverloadedStrings , this causes "foo" to be transformed into fromString "foo" for whatever fromString 's in scope, not necessarily Data.String.fromString "foo" . So making fromString synonymous with pack will do what you want. A complete example:

{-# LANGUAGE OverloadedStrings, RebindableSyntax #-} import Prelude import qualified Data.Text as T import qualified Data.Text.IO as T fromString :: String -> T.Text fromString = T.pack main :: IO () main = T.putStrLn "Hello, world!"

This works fine, and changing main to main = putStrLn "Hello, world!" produces the desired error:

TestStrings.hs:11:17: Couldn't match expected type `String' with actual type `T.Text' Expected type: [Char] -> String Actual type: String -> T.Text In the first argument of `putStrLn', namely `"Hello, world!"' In the expression: putStrLn "Hello, world!"

Commenting out the definition of fromString causes a different error:

TestStrings.hs:11:19: Not in scope: `fromString' Perhaps you meant `showString' (imported from Prelude)

If you want it to work with both strict and lazy text, you could define your own IsString type class, and make both of them instances; the class doesn't have to be called IsString , just so long as it has a fromString method.